Merge "[CameraViewfinder] Add new public constructor for ViewfinderSurfaceRequest." 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/PREUPLOAD.cfg b/PREUPLOAD.cfg
index f5a9b59..36c6362 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,5 +1,5 @@
 [Hook Scripts]
-checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT} -c ${REPO_ROOT}/frameworks/support/development/checkstyle/config/support-lib.xml -p development/checkstyle/prebuilt/com.android.support.checkstyle.jar
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
 ktlint_hook = ${REPO_ROOT}/frameworks/support/development/ktlint.sh --skip-if-empty --file=${PREUPLOAD_FILES_PREFIXED}
 warn_check_api = ${REPO_ROOT}/frameworks/support/development/apilint.py -f ${PREUPLOAD_FILES}
 
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..6675a85 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
@@ -121,7 +121,7 @@
             val dispatcherOwner = object : OnBackPressedDispatcherOwner {
                 override fun getLifecycle() = lifecycleOwner.lifecycle
 
-                override fun getOnBackPressedDispatcher() = dispatcher
+                override val onBackPressedDispatcher = dispatcher
             }
             dispatcher.addCallback(lifecycleOwner) { }
             CompositionLocalProvider(
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..26a2d46 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
@@ -60,9 +60,7 @@
                     return LifecycleRegistry(this)
                 }
 
-                override fun getOnBackPressedDispatcher(): OnBackPressedDispatcher {
-                    return OnBackPressedDispatcher()
-                }
+                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.txt b/activity/activity/api/current.txt
index 078f6f9..b149667 100644
--- a/activity/activity/api/current.txt
+++ b/activity/activity/api/current.txt
@@ -52,6 +52,7 @@
     method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
     method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
     method @CallSuper public void onBackPressed();
+    property public final androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
     property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
   }
 
@@ -84,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 {
@@ -174,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 {
diff --git a/activity/activity/api/public_plus_experimental_current.txt b/activity/activity/api/public_plus_experimental_current.txt
index 078f6f9..b149667 100644
--- a/activity/activity/api/public_plus_experimental_current.txt
+++ b/activity/activity/api/public_plus_experimental_current.txt
@@ -52,6 +52,7 @@
     method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
     method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
     method @CallSuper public void onBackPressed();
+    property public final androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
     property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
   }
 
@@ -84,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 {
@@ -174,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 {
diff --git a/activity/activity/api/restricted_current.txt b/activity/activity/api/restricted_current.txt
index 575b8b9..0be4796 100644
--- a/activity/activity/api/restricted_current.txt
+++ b/activity/activity/api/restricted_current.txt
@@ -51,6 +51,7 @@
     method public final androidx.activity.OnBackPressedDispatcher getOnBackPressedDispatcher();
     method public androidx.savedstate.SavedStateRegistry getSavedStateRegistry();
     method @CallSuper public void onBackPressed();
+    property public final androidx.activity.OnBackPressedDispatcher onBackPressedDispatcher;
     property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
   }
 
@@ -83,17 +84,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 +179,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 {
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index 8a8e97b..821f0d8 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -30,6 +30,7 @@
     api(libs.kotlinStdlib)
 
     androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
+    androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
     androidTestImplementation(libs.kotlinStdlib)
     androidTestImplementation(libs.espressoCore, excludes.espresso)
     androidTestImplementation(libs.leakcanary)
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt
index 7b9523f..fe85d84 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt
@@ -21,8 +21,8 @@
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import android.widget.TextView
 import androidx.activity.test.R
-import androidx.lifecycle.ViewTreeViewModelStoreOwner
 import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
 import androidx.savedstate.findViewTreeSavedStateRegistryOwner
 import androidx.test.core.app.ActivityScenario
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -62,7 +62,7 @@
                     .that(inflatedTextView.findViewTreeLifecycleOwner())
                     .isSameInstanceAs(this@withActivity)
                 assertWithMessage("inflated view has correct ViewTreeViewModelStoreOwner")
-                    .that(ViewTreeViewModelStoreOwner.get(inflatedTextView))
+                    .that(inflatedTextView.findViewTreeViewModelStoreOwner())
                     .isSameInstanceAs(this@withActivity)
                 assertWithMessage("inflated view has correct ViewTreeSavedStateRegistryOwner")
                     .that(inflatedTextView.findViewTreeSavedStateRegistryOwner())
@@ -100,7 +100,7 @@
 
                     override fun onViewAttachedToWindow(v: View) {
                         attachedLifecycleOwner = view.findViewTreeLifecycleOwner()
-                        attachedViewModelStoreOwner = ViewTreeViewModelStoreOwner.get(view)
+                        attachedViewModelStoreOwner = view.findViewTreeViewModelStoreOwner()
                         attachedSavedStateRegistryOwner = view.findViewTreeSavedStateRegistryOwner()
                     }
                 })
diff --git a/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..7eded1b 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ViewTreeOnBackPressedDispatcherTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ViewTreeOnBackPressedDispatcherTest.kt
@@ -119,8 +119,7 @@
             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..c67bb77 100644
--- a/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt
+++ b/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt
@@ -89,12 +89,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/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/appactions/interaction/interaction-capabilities-core/api/current.txt b/appactions/interaction/interaction-capabilities-core/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-capabilities-core/api/public_plus_experimental_current.txt b/appactions/interaction/interaction-capabilities-core/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-capabilities-core/api/res-current.txt b/appactions/interaction/interaction-capabilities-core/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/api/res-current.txt
diff --git a/appactions/interaction/interaction-capabilities-core/api/restricted_current.txt b/appactions/interaction/interaction-capabilities-core/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-capabilities-core/build.gradle b/appactions/interaction/interaction-capabilities-core/build.gradle
new file mode 100644
index 0000000..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/appactions/interaction/interaction-service/api/current.txt b/appactions/interaction/interaction-service/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-service/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-service/api/public_plus_experimental_current.txt b/appactions/interaction/interaction-service/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-service/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-service/api/res-current.txt b/appactions/interaction/interaction-service/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/appactions/interaction/interaction-service/api/res-current.txt
diff --git a/appactions/interaction/interaction-service/api/restricted_current.txt b/appactions/interaction/interaction-service/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-service/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
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 5bd48ef..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,8 +21,8 @@
 import android.content.ComponentName
 import android.content.Intent
 import android.content.pm.PackageManager
+import android.os.Build
 import android.os.LocaleList
-import androidx.annotation.RequiresApi
 import androidx.appcompat.testutils.LocalesActivityTestRule
 import androidx.appcompat.testutils.LocalesUtils.CUSTOM_LOCALE_LIST
 import androidx.appcompat.testutils.LocalesUtils.assertConfigurationLocalesEquals
@@ -35,7 +35,6 @@
 import junit.framework.Assert.assertNull
 import org.junit.After
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -54,7 +53,6 @@
     private lateinit var appLocalesComponent: ComponentName
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
 
-    @RequiresApi(33)
     @Before
     fun setUp() {
         // setting the app to follow system.
@@ -78,11 +76,12 @@
         )
     }
 
-    @Ignore // b/264589466
-    @RequiresApi(33)
     @Test
-    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
     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.
@@ -140,7 +139,6 @@
     }
 
     @After
-    @RequiresApi(33)
     fun teardown() {
         val context = instrumentation.context
 
@@ -163,4 +161,4 @@
             /* flags= */ PackageManager.DONT_KILL_APP
         )
     }
-}
\ No newline at end of file
+}
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/AlwaysSupportedFeatures.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
index e011d02..f8c6226 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
@@ -43,7 +43,7 @@
             case Features.JOIN_SPEC_AND_QUALIFIED_ID:
                 // fall through
             case Features.NUMERIC_SEARCH:
-                //fall through
+                // fall through
             case Features.VERBATIM_SEARCH:
                 // fall through
             case Features.SEARCH_RESULT_MATCH_INFO_SUBMATCH:
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/FeaturesImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
index 6ac3760..9e70974 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
@@ -57,13 +57,15 @@
                 // synced over into service-appsearch.
                 // fall through
             case SEARCH_SPEC_ADVANCED_RANKING_EXPRESSION:
-                // TODO(b/261474063): Update to reflect support in Android U+ once advanced
+                // TODO(b/261474063) : Update to reflect support in Android U+ once advanced
                 //  ranking becomes available.
                 // fall through
             case Features.JOIN_SPEC_AND_QUALIFIED_ID:
                 // TODO(b/256022027) : Update to reflect support in Android U+ once this feature is
+                // synced over into service-appsearch.
+                // fall through
             case Features.VERBATIM_SEARCH:
-                // TODO(b/204333391): Update to reflect support in Android U+ once this feature is
+                // TODO(b/204333391) : Update to reflect support in Android U+ once this feature is
                 // synced over into service-appsearch.
                 return false;
             default:
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/api/current.txt b/appsearch/appsearch/api/current.txt
index 98635d6..f747bf5 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -523,7 +523,7 @@
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionPathsForDocumentClass(Class<?>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionsForDocumentClass(Class<?>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec build();
-    method public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_PROPERTY_WEIGHTS) public androidx.appsearch.app.SearchSpec.Builder setPropertyWeightPaths(String, java.util.Map<androidx.appsearch.app.PropertyPath!,java.lang.Double!>);
diff --git a/appsearch/appsearch/api/public_plus_experimental_current.txt b/appsearch/appsearch/api/public_plus_experimental_current.txt
index 98635d6..f747bf5 100644
--- a/appsearch/appsearch/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch/api/public_plus_experimental_current.txt
@@ -523,7 +523,7 @@
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionPathsForDocumentClass(Class<?>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionsForDocumentClass(Class<?>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec build();
-    method public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_PROPERTY_WEIGHTS) public androidx.appsearch.app.SearchSpec.Builder setPropertyWeightPaths(String, java.util.Map<androidx.appsearch.app.PropertyPath!,java.lang.Double!>);
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index 98635d6..f747bf5 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -523,7 +523,7 @@
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionPathsForDocumentClass(Class<?>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionsForDocumentClass(Class<?>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec build();
-    method public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);
+    method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.JOIN_SPEC_AND_QUALIFIED_ID) public androidx.appsearch.app.SearchSpec.Builder setJoinSpec(androidx.appsearch.app.JoinSpec);
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
     method @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.SEARCH_SPEC_PROPERTY_WEIGHTS) public androidx.appsearch.app.SearchSpec.Builder setPropertyWeightPaths(String, java.util.Map<androidx.appsearch.app.PropertyPath!,java.lang.Double!>);
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/AppSearchSchema.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
index dfcc149..8a05b92 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
@@ -570,14 +570,14 @@
         public static final int JOINABLE_VALUE_TYPE_NONE = 0;
 
         /**
-         * Content in this property will be used as string qualified id to join documents.
+         * Content in this string property will be used as a qualified id to join documents.
          * <ul>
          *     <li>Qualified id: a unique identifier for a document, and this joinable value type is
          *     similar to primary and foreign key in relational database. See
-         *     {@link androidx.appsearch.util.DocumentIdUtil} for more details.</li>
+         *     {@link androidx.appsearch.util.DocumentIdUtil} for more details.
          *     <li>Currently we only support single string joining, so it should only be used with
-         *     {@link PropertyConfig.Cardinality} other than
-         *     {@link PropertyConfig#CARDINALITY_REPEATED}.
+         *     {@link PropertyConfig#CARDINALITY_OPTIONAL} and
+         *     {@link PropertyConfig#CARDINALITY_REQUIRED}.
          * </ul>
          */
         // @exportToFramework:startStrip()
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/Features.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
index bc891fe..d3fcbba 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
@@ -104,8 +104,8 @@
 
     /**
      * Feature for {@link #isFeatureSupported(String)}. This feature covers
-     * {@link AppSearchSchema.StringPropertyConfig#JOINABLE_VALUE_TYPE_QUALIFIED_ID} and all other
-     * join features.
+     * {@link AppSearchSchema.StringPropertyConfig#JOINABLE_VALUE_TYPE_QUALIFIED_ID},
+     * {@link SearchSpec.Builder#setJoinSpec}, and all other join features.
      */
     String JOIN_SPEC_AND_QUALIFIED_ID = "JOIN_SPEC_AND_QUALIFIED_ID";
 
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 eb9a30c..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) {
@@ -1071,6 +1073,11 @@
          *
          * @param joinSpec a specification on how to perform the Join operation.
          */
+        // @exportToFramework:startStrip()
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.JOIN_SPEC_AND_QUALIFIED_ID)
+        // @exportToFramework:endStrip()
         @NonNull
         public Builder setJoinSpec(@NonNull JoinSpec joinSpec) {
             resetIfBuilt();
@@ -1108,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) {
@@ -1232,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/arch/core/core-runtime/api/2.2.0-beta01.txt b/arch/core/core-runtime/api/2.2.0-beta01.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/arch/core/core-runtime/api/2.2.0-beta01.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/arch/core/core-runtime/api/public_plus_experimental_2.2.0-beta01.txt b/arch/core/core-runtime/api/public_plus_experimental_2.2.0-beta01.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/arch/core/core-runtime/api/public_plus_experimental_2.2.0-beta01.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/arch/core/core-runtime/api/res-2.2.0-beta01.txt b/arch/core/core-runtime/api/res-2.2.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/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/arch/core/core-testing/api/res-2.2.0-beta01.txt b/arch/core/core-testing/api/res-2.2.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/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/PerfettoSdkHandshakeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
index 368cacf..65ac589 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
@@ -47,7 +47,7 @@
 import org.junit.runners.Parameterized
 import org.junit.runners.Parameterized.Parameters
 
-private const val tracingPerfettoVersion = "1.0.0-alpha09" // TODO(224510255): get by 'reflection'
+private const val tracingPerfettoVersion = "1.0.0-alpha10" // TODO(224510255): get by 'reflection'
 private const val minSupportedSdk = Build.VERSION_CODES.R // TODO(234351579): Support API < 30
 
 @RunWith(Parameterized::class)
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/browser/browser/api/1.5.0-beta02.txt b/browser/browser/api/1.5.0-beta02.txt
new file mode 100644
index 0000000..0598270
--- /dev/null
+++ b/browser/browser/api/1.5.0-beta02.txt
@@ -0,0 +1,469 @@
+// Signature format: 4.0
+package androidx.browser.browseractions {
+
+  @Deprecated public class BrowserActionItem {
+    ctor @Deprecated public BrowserActionItem(String, android.app.PendingIntent, @DrawableRes int);
+    ctor @Deprecated public BrowserActionItem(String, android.app.PendingIntent);
+    method @Deprecated public android.app.PendingIntent getAction();
+    method @Deprecated public int getIconId();
+    method @Deprecated public String getTitle();
+  }
+
+  @Deprecated public class BrowserActionsIntent {
+    method @Deprecated public static String? getCreatorPackageName(android.content.Intent);
+    method @Deprecated public android.content.Intent getIntent();
+    method @Deprecated public static String? getUntrustedCreatorPackageName(android.content.Intent);
+    method @Deprecated public static void launchIntent(android.content.Context, android.content.Intent);
+    method @Deprecated public static void openBrowserAction(android.content.Context, android.net.Uri);
+    method @Deprecated public static void openBrowserAction(android.content.Context, android.net.Uri, int, java.util.ArrayList<androidx.browser.browseractions.BrowserActionItem!>, android.app.PendingIntent);
+    method @Deprecated public static java.util.List<androidx.browser.browseractions.BrowserActionItem!> parseBrowserActionItems(java.util.ArrayList<android.os.Bundle!>);
+    field @Deprecated public static final String ACTION_BROWSER_ACTIONS_OPEN = "androidx.browser.browseractions.browser_action_open";
+    field @Deprecated public static final String EXTRA_APP_ID = "androidx.browser.browseractions.APP_ID";
+    field @Deprecated public static final String EXTRA_MENU_ITEMS = "androidx.browser.browseractions.extra.MENU_ITEMS";
+    field @Deprecated public static final String EXTRA_SELECTED_ACTION_PENDING_INTENT = "androidx.browser.browseractions.extra.SELECTED_ACTION_PENDING_INTENT";
+    field @Deprecated public static final String EXTRA_TYPE = "androidx.browser.browseractions.extra.TYPE";
+    field @Deprecated public static final int ITEM_COPY = 3; // 0x3
+    field @Deprecated public static final int ITEM_DOWNLOAD = 2; // 0x2
+    field @Deprecated public static final int ITEM_INVALID_ITEM = -1; // 0xffffffff
+    field @Deprecated public static final int ITEM_OPEN_IN_INCOGNITO = 1; // 0x1
+    field @Deprecated public static final int ITEM_OPEN_IN_NEW_TAB = 0; // 0x0
+    field @Deprecated public static final int ITEM_SHARE = 4; // 0x4
+    field @Deprecated public static final String KEY_ACTION = "androidx.browser.browseractions.ACTION";
+    field @Deprecated public static final String KEY_ICON_ID = "androidx.browser.browseractions.ICON_ID";
+    field @Deprecated public static final String KEY_TITLE = "androidx.browser.browseractions.TITLE";
+    field @Deprecated public static final int MAX_CUSTOM_ITEMS = 5; // 0x5
+    field @Deprecated public static final int URL_TYPE_AUDIO = 3; // 0x3
+    field @Deprecated public static final int URL_TYPE_FILE = 4; // 0x4
+    field @Deprecated public static final int URL_TYPE_IMAGE = 1; // 0x1
+    field @Deprecated public static final int URL_TYPE_NONE = 0; // 0x0
+    field @Deprecated public static final int URL_TYPE_PLUGIN = 5; // 0x5
+    field @Deprecated public static final int URL_TYPE_VIDEO = 2; // 0x2
+  }
+
+  @Deprecated public static final class BrowserActionsIntent.Builder {
+    ctor @Deprecated public BrowserActionsIntent.Builder(android.content.Context, android.net.Uri);
+    method @Deprecated public androidx.browser.browseractions.BrowserActionsIntent build();
+    method @Deprecated public androidx.browser.browseractions.BrowserActionsIntent.Builder setCustomItems(java.util.ArrayList<androidx.browser.browseractions.BrowserActionItem!>);
+    method @Deprecated public androidx.browser.browseractions.BrowserActionsIntent.Builder setCustomItems(androidx.browser.browseractions.BrowserActionItem!...);
+    method @Deprecated public androidx.browser.browseractions.BrowserActionsIntent.Builder setOnItemSelectedAction(android.app.PendingIntent);
+    method @Deprecated public androidx.browser.browseractions.BrowserActionsIntent.Builder setUrlType(int);
+  }
+
+}
+
+package androidx.browser.customtabs {
+
+  public final class CustomTabColorSchemeParams {
+    field @ColorInt public final Integer? navigationBarColor;
+    field @ColorInt public final Integer? navigationBarDividerColor;
+    field @ColorInt public final Integer? secondaryToolbarColor;
+    field @ColorInt public final Integer? toolbarColor;
+  }
+
+  public static final class CustomTabColorSchemeParams.Builder {
+    ctor public CustomTabColorSchemeParams.Builder();
+    method public androidx.browser.customtabs.CustomTabColorSchemeParams build();
+    method public androidx.browser.customtabs.CustomTabColorSchemeParams.Builder setNavigationBarColor(@ColorInt int);
+    method public androidx.browser.customtabs.CustomTabColorSchemeParams.Builder setNavigationBarDividerColor(@ColorInt int);
+    method public androidx.browser.customtabs.CustomTabColorSchemeParams.Builder setSecondaryToolbarColor(@ColorInt int);
+    method public androidx.browser.customtabs.CustomTabColorSchemeParams.Builder setToolbarColor(@ColorInt int);
+  }
+
+  public class CustomTabsCallback {
+    ctor public CustomTabsCallback();
+    method public void extraCallback(String, android.os.Bundle?);
+    method public android.os.Bundle? extraCallbackWithResult(String, android.os.Bundle?);
+    method public void onActivityResized(@Dimension(unit=androidx.annotation.Dimension.PX) int, @Dimension(unit=androidx.annotation.Dimension.PX) int, android.os.Bundle);
+    method public void onMessageChannelReady(android.os.Bundle?);
+    method public void onNavigationEvent(int, android.os.Bundle?);
+    method public void onPostMessage(String, android.os.Bundle?);
+    method public void onRelationshipValidationResult(@androidx.browser.customtabs.CustomTabsService.Relation int, android.net.Uri, boolean, android.os.Bundle?);
+    field public static final int NAVIGATION_ABORTED = 4; // 0x4
+    field public static final int NAVIGATION_FAILED = 3; // 0x3
+    field public static final int NAVIGATION_FINISHED = 2; // 0x2
+    field public static final int NAVIGATION_STARTED = 1; // 0x1
+    field public static final int TAB_HIDDEN = 6; // 0x6
+    field public static final int TAB_SHOWN = 5; // 0x5
+  }
+
+  public class CustomTabsClient {
+    method public static boolean bindCustomTabsService(android.content.Context, String?, androidx.browser.customtabs.CustomTabsServiceConnection);
+    method public static boolean bindCustomTabsServicePreservePriority(android.content.Context, String?, androidx.browser.customtabs.CustomTabsServiceConnection);
+    method public static boolean connectAndInitialize(android.content.Context, String);
+    method public android.os.Bundle? extraCommand(String, android.os.Bundle?);
+    method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?);
+    method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?, boolean);
+    method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?);
+    method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?, int);
+    method public boolean warmup(long);
+  }
+
+  public final class CustomTabsIntent {
+    method public static int getActivityResizeBehavior(android.content.Intent);
+    method public static int getCloseButtonPosition(android.content.Intent);
+    method public static androidx.browser.customtabs.CustomTabColorSchemeParams getColorSchemeParams(android.content.Intent, int);
+    method @Dimension(unit=androidx.annotation.Dimension.PX) public static int getInitialActivityHeightPx(android.content.Intent);
+    method public static int getMaxToolbarItems();
+    method @Dimension(unit=androidx.annotation.Dimension.DP) public static int getToolbarCornerRadiusDp(android.content.Intent);
+    method public void launchUrl(android.content.Context, android.net.Uri);
+    method public static android.content.Intent setAlwaysUseBrowserUI(android.content.Intent?);
+    method public static boolean shouldAlwaysUseBrowserUI(android.content.Intent);
+    field public static final int ACTIVITY_HEIGHT_ADJUSTABLE = 1; // 0x1
+    field public static final int ACTIVITY_HEIGHT_DEFAULT = 0; // 0x0
+    field public static final int ACTIVITY_HEIGHT_FIXED = 2; // 0x2
+    field public static final int CLOSE_BUTTON_POSITION_DEFAULT = 0; // 0x0
+    field public static final int CLOSE_BUTTON_POSITION_END = 2; // 0x2
+    field public static final int CLOSE_BUTTON_POSITION_START = 1; // 0x1
+    field public static final int COLOR_SCHEME_DARK = 2; // 0x2
+    field public static final int COLOR_SCHEME_LIGHT = 1; // 0x1
+    field public static final int COLOR_SCHEME_SYSTEM = 0; // 0x0
+    field public static final String EXTRA_ACTION_BUTTON_BUNDLE = "android.support.customtabs.extra.ACTION_BUTTON_BUNDLE";
+    field public static final String EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR = "androidx.browser.customtabs.extra.ACTIVITY_HEIGHT_RESIZE_BEHAVIOR";
+    field public static final String EXTRA_CLOSE_BUTTON_ICON = "android.support.customtabs.extra.CLOSE_BUTTON_ICON";
+    field public static final String EXTRA_CLOSE_BUTTON_POSITION = "androidx.browser.customtabs.extra.CLOSE_BUTTON_POSITION";
+    field public static final String EXTRA_COLOR_SCHEME = "androidx.browser.customtabs.extra.COLOR_SCHEME";
+    field public static final String EXTRA_COLOR_SCHEME_PARAMS = "androidx.browser.customtabs.extra.COLOR_SCHEME_PARAMS";
+    field @Deprecated public static final String EXTRA_DEFAULT_SHARE_MENU_ITEM = "android.support.customtabs.extra.SHARE_MENU_ITEM";
+    field public static final String EXTRA_ENABLE_INSTANT_APPS = "android.support.customtabs.extra.EXTRA_ENABLE_INSTANT_APPS";
+    field public static final String EXTRA_ENABLE_URLBAR_HIDING = "android.support.customtabs.extra.ENABLE_URLBAR_HIDING";
+    field public static final String EXTRA_EXIT_ANIMATION_BUNDLE = "android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE";
+    field public static final String EXTRA_INITIAL_ACTIVITY_HEIGHT_PX = "androidx.browser.customtabs.extra.INITIAL_ACTIVITY_HEIGHT_PX";
+    field public static final String EXTRA_MENU_ITEMS = "android.support.customtabs.extra.MENU_ITEMS";
+    field public static final String EXTRA_NAVIGATION_BAR_COLOR = "androidx.browser.customtabs.extra.NAVIGATION_BAR_COLOR";
+    field public static final String EXTRA_NAVIGATION_BAR_DIVIDER_COLOR = "androidx.browser.customtabs.extra.NAVIGATION_BAR_DIVIDER_COLOR";
+    field public static final String EXTRA_REMOTEVIEWS = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS";
+    field public static final String EXTRA_REMOTEVIEWS_CLICKED_ID = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_CLICKED_ID";
+    field public static final String EXTRA_REMOTEVIEWS_PENDINGINTENT = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_PENDINGINTENT";
+    field public static final String EXTRA_REMOTEVIEWS_VIEW_IDS = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_VIEW_IDS";
+    field public static final String EXTRA_SECONDARY_TOOLBAR_COLOR = "android.support.customtabs.extra.SECONDARY_TOOLBAR_COLOR";
+    field public static final String EXTRA_SESSION = "android.support.customtabs.extra.SESSION";
+    field public static final String EXTRA_SHARE_STATE = "androidx.browser.customtabs.extra.SHARE_STATE";
+    field public static final String EXTRA_TINT_ACTION_BUTTON = "android.support.customtabs.extra.TINT_ACTION_BUTTON";
+    field public static final String EXTRA_TITLE_VISIBILITY_STATE = "android.support.customtabs.extra.TITLE_VISIBILITY";
+    field public static final String EXTRA_TOOLBAR_COLOR = "android.support.customtabs.extra.TOOLBAR_COLOR";
+    field public static final String EXTRA_TOOLBAR_CORNER_RADIUS_DP = "androidx.browser.customtabs.extra.TOOLBAR_CORNER_RADIUS_DP";
+    field public static final String EXTRA_TOOLBAR_ITEMS = "android.support.customtabs.extra.TOOLBAR_ITEMS";
+    field public static final String KEY_DESCRIPTION = "android.support.customtabs.customaction.DESCRIPTION";
+    field public static final String KEY_ICON = "android.support.customtabs.customaction.ICON";
+    field public static final String KEY_ID = "android.support.customtabs.customaction.ID";
+    field public static final String KEY_MENU_ITEM_TITLE = "android.support.customtabs.customaction.MENU_ITEM_TITLE";
+    field public static final String KEY_PENDING_INTENT = "android.support.customtabs.customaction.PENDING_INTENT";
+    field public static final int NO_TITLE = 0; // 0x0
+    field public static final int SHARE_STATE_DEFAULT = 0; // 0x0
+    field public static final int SHARE_STATE_OFF = 2; // 0x2
+    field public static final int SHARE_STATE_ON = 1; // 0x1
+    field public static final int SHOW_PAGE_TITLE = 1; // 0x1
+    field public static final int TOOLBAR_ACTION_BUTTON_ID = 0; // 0x0
+    field public final android.content.Intent intent;
+    field public final android.os.Bundle? startAnimationBundle;
+  }
+
+  public static final class CustomTabsIntent.Builder {
+    ctor public CustomTabsIntent.Builder();
+    ctor public CustomTabsIntent.Builder(androidx.browser.customtabs.CustomTabsSession?);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder addDefaultShareMenuItem();
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder addMenuItem(String, android.app.PendingIntent);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder addToolbarItem(int, android.graphics.Bitmap, String, android.app.PendingIntent) throws java.lang.IllegalStateException;
+    method public androidx.browser.customtabs.CustomTabsIntent build();
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder enableUrlBarHiding();
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setActionButton(android.graphics.Bitmap, String, android.app.PendingIntent, boolean);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setActionButton(android.graphics.Bitmap, String, android.app.PendingIntent);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setCloseButtonIcon(android.graphics.Bitmap);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setCloseButtonPosition(int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setColorScheme(int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setColorSchemeParams(int, androidx.browser.customtabs.CustomTabColorSchemeParams);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setDefaultColorSchemeParams(androidx.browser.customtabs.CustomTabColorSchemeParams);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setDefaultShareMenuItemEnabled(boolean);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setExitAnimations(android.content.Context, @AnimRes int, @AnimRes int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setInitialActivityHeightPx(@Dimension(unit=androidx.annotation.Dimension.PX) int, int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setInitialActivityHeightPx(@Dimension(unit=androidx.annotation.Dimension.PX) int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setInstantAppsEnabled(boolean);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setNavigationBarColor(@ColorInt int);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setNavigationBarDividerColor(@ColorInt int);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarColor(@ColorInt int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarViews(android.widget.RemoteViews, int[]?, android.app.PendingIntent?);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setSession(androidx.browser.customtabs.CustomTabsSession);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setShareState(int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setShowTitle(boolean);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setStartAnimations(android.content.Context, @AnimRes int, @AnimRes int);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setToolbarColor(@ColorInt int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setToolbarCornerRadiusDp(@Dimension(unit=androidx.annotation.Dimension.DP) int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setUrlBarHidingEnabled(boolean);
+  }
+
+  public abstract class CustomTabsService extends android.app.Service {
+    ctor public CustomTabsService();
+    method protected boolean cleanUpSession(androidx.browser.customtabs.CustomTabsSessionToken);
+    method protected abstract android.os.Bundle? extraCommand(String, android.os.Bundle?);
+    method protected abstract boolean mayLaunchUrl(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri?, android.os.Bundle?, java.util.List<android.os.Bundle!>?);
+    method protected abstract boolean newSession(androidx.browser.customtabs.CustomTabsSessionToken);
+    method public android.os.IBinder onBind(android.content.Intent?);
+    method @androidx.browser.customtabs.CustomTabsService.Result protected abstract int postMessage(androidx.browser.customtabs.CustomTabsSessionToken, String, android.os.Bundle?);
+    method protected abstract boolean receiveFile(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri, int, android.os.Bundle?);
+    method protected abstract boolean requestPostMessageChannel(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri);
+    method protected abstract boolean updateVisuals(androidx.browser.customtabs.CustomTabsSessionToken, android.os.Bundle?);
+    method protected abstract boolean validateRelationship(androidx.browser.customtabs.CustomTabsSessionToken, @androidx.browser.customtabs.CustomTabsService.Relation int, android.net.Uri, android.os.Bundle?);
+    method protected abstract boolean warmup(long);
+    field public static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService";
+    field public static final String CATEGORY_COLOR_SCHEME_CUSTOMIZATION = "androidx.browser.customtabs.category.ColorSchemeCustomization";
+    field public static final String CATEGORY_NAVBAR_COLOR_CUSTOMIZATION = "androidx.browser.customtabs.category.NavBarColorCustomization";
+    field public static final String CATEGORY_TRUSTED_WEB_ACTIVITY_IMMERSIVE_MODE = "androidx.browser.trusted.category.ImmersiveMode";
+    field public static final String CATEGORY_WEB_SHARE_TARGET_V2 = "androidx.browser.trusted.category.WebShareTargetV2";
+    field public static final int FILE_PURPOSE_TRUSTED_WEB_ACTIVITY_SPLASH_IMAGE = 1; // 0x1
+    field public static final String KEY_SUCCESS = "androidx.browser.customtabs.SUCCESS";
+    field public static final String KEY_URL = "android.support.customtabs.otherurls.URL";
+    field public static final int RELATION_HANDLE_ALL_URLS = 2; // 0x2
+    field public static final int RELATION_USE_AS_ORIGIN = 1; // 0x1
+    field public static final int RESULT_FAILURE_DISALLOWED = -1; // 0xffffffff
+    field public static final int RESULT_FAILURE_MESSAGING_ERROR = -3; // 0xfffffffd
+    field public static final int RESULT_FAILURE_REMOTE_ERROR = -2; // 0xfffffffe
+    field public static final int RESULT_SUCCESS = 0; // 0x0
+    field public static final String TRUSTED_WEB_ACTIVITY_CATEGORY = "androidx.browser.trusted.category.TrustedWebActivities";
+  }
+
+  @IntDef({androidx.browser.customtabs.CustomTabsService.RELATION_USE_AS_ORIGIN, androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface CustomTabsService.Relation {
+  }
+
+  @IntDef({androidx.browser.customtabs.CustomTabsService.RESULT_SUCCESS, androidx.browser.customtabs.CustomTabsService.RESULT_FAILURE_DISALLOWED, androidx.browser.customtabs.CustomTabsService.RESULT_FAILURE_REMOTE_ERROR, androidx.browser.customtabs.CustomTabsService.RESULT_FAILURE_MESSAGING_ERROR}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface CustomTabsService.Result {
+  }
+
+  public abstract class CustomTabsServiceConnection implements android.content.ServiceConnection {
+    ctor public CustomTabsServiceConnection();
+    method public abstract void onCustomTabsServiceConnected(android.content.ComponentName, androidx.browser.customtabs.CustomTabsClient);
+    method public final void onServiceConnected(android.content.ComponentName, android.os.IBinder);
+  }
+
+  public final class CustomTabsSession {
+    method @VisibleForTesting public static androidx.browser.customtabs.CustomTabsSession createMockSessionForTesting(android.content.ComponentName);
+    method public boolean mayLaunchUrl(android.net.Uri?, android.os.Bundle?, java.util.List<android.os.Bundle!>?);
+    method @androidx.browser.customtabs.CustomTabsService.Result public int postMessage(String, android.os.Bundle?);
+    method public boolean receiveFile(android.net.Uri, int, android.os.Bundle?);
+    method public boolean requestPostMessageChannel(android.net.Uri);
+    method public boolean setActionButton(android.graphics.Bitmap, String);
+    method public boolean setSecondaryToolbarViews(android.widget.RemoteViews?, int[]?, android.app.PendingIntent?);
+    method @Deprecated public boolean setToolbarItem(int, android.graphics.Bitmap, String);
+    method public boolean validateRelationship(@androidx.browser.customtabs.CustomTabsService.Relation int, android.net.Uri, android.os.Bundle?);
+  }
+
+  public class CustomTabsSessionToken {
+    method public static androidx.browser.customtabs.CustomTabsSessionToken createMockSessionTokenForTesting();
+    method public androidx.browser.customtabs.CustomTabsCallback? getCallback();
+    method public static androidx.browser.customtabs.CustomTabsSessionToken? getSessionTokenFromIntent(android.content.Intent);
+    method public boolean isAssociatedWith(androidx.browser.customtabs.CustomTabsSession);
+  }
+
+  public class PostMessageService extends android.app.Service {
+    ctor public PostMessageService();
+    method public android.os.IBinder onBind(android.content.Intent?);
+  }
+
+  public abstract class PostMessageServiceConnection implements android.content.ServiceConnection {
+    ctor public PostMessageServiceConnection(androidx.browser.customtabs.CustomTabsSessionToken);
+    method public boolean bindSessionToPostMessageService(android.content.Context, String);
+    method public final boolean notifyMessageChannelReady(android.os.Bundle?);
+    method public void onPostMessageServiceConnected();
+    method public void onPostMessageServiceDisconnected();
+    method public final void onServiceConnected(android.content.ComponentName, android.os.IBinder);
+    method public final void onServiceDisconnected(android.content.ComponentName);
+    method public final boolean postMessage(String, android.os.Bundle?);
+    method public void unbindFromContext(android.content.Context);
+  }
+
+  public class TrustedWebUtils {
+    method public static boolean areSplashScreensSupported(android.content.Context, String, String);
+    method @Deprecated public static void launchAsTrustedWebActivity(android.content.Context, androidx.browser.customtabs.CustomTabsIntent, android.net.Uri);
+    method @WorkerThread public static boolean transferSplashImage(android.content.Context, java.io.File, String, String, androidx.browser.customtabs.CustomTabsSession);
+    field public static final String EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY = "android.support.customtabs.extra.LAUNCH_AS_TRUSTED_WEB_ACTIVITY";
+  }
+
+}
+
+package androidx.browser.trusted {
+
+  public final class ScreenOrientation {
+    field public static final int ANY = 5; // 0x5
+    field public static final int DEFAULT = 0; // 0x0
+    field public static final int LANDSCAPE = 6; // 0x6
+    field public static final int LANDSCAPE_PRIMARY = 3; // 0x3
+    field public static final int LANDSCAPE_SECONDARY = 4; // 0x4
+    field public static final int NATURAL = 8; // 0x8
+    field public static final int PORTRAIT = 7; // 0x7
+    field public static final int PORTRAIT_PRIMARY = 1; // 0x1
+    field public static final int PORTRAIT_SECONDARY = 2; // 0x2
+  }
+
+  public final class Token {
+    method public static androidx.browser.trusted.Token? create(String, android.content.pm.PackageManager);
+    method public static androidx.browser.trusted.Token deserialize(byte[]);
+    method public boolean matches(String, android.content.pm.PackageManager);
+    method public byte[] serialize();
+  }
+
+  public interface TokenStore {
+    method @BinderThread public androidx.browser.trusted.Token? load();
+    method @WorkerThread public void store(androidx.browser.trusted.Token?);
+  }
+
+  public abstract class TrustedWebActivityCallback {
+    ctor public TrustedWebActivityCallback();
+    method public abstract void onExtraCallback(String, android.os.Bundle?);
+  }
+
+  public class TrustedWebActivityCallbackRemote {
+    method public void runExtraCallback(String, android.os.Bundle) throws android.os.RemoteException;
+  }
+
+  public interface TrustedWebActivityDisplayMode {
+    method public static androidx.browser.trusted.TrustedWebActivityDisplayMode fromBundle(android.os.Bundle);
+    method public android.os.Bundle toBundle();
+    field public static final String KEY_ID = "androidx.browser.trusted.displaymode.KEY_ID";
+  }
+
+  public static class TrustedWebActivityDisplayMode.DefaultMode implements androidx.browser.trusted.TrustedWebActivityDisplayMode {
+    ctor public TrustedWebActivityDisplayMode.DefaultMode();
+    method public android.os.Bundle toBundle();
+  }
+
+  public static class TrustedWebActivityDisplayMode.ImmersiveMode implements androidx.browser.trusted.TrustedWebActivityDisplayMode {
+    ctor public TrustedWebActivityDisplayMode.ImmersiveMode(boolean, int);
+    method public boolean isSticky();
+    method public int layoutInDisplayCutoutMode();
+    method public android.os.Bundle toBundle();
+    field public static final String KEY_CUTOUT_MODE = "androidx.browser.trusted.displaymode.KEY_CUTOUT_MODE";
+    field public static final String KEY_STICKY = "androidx.browser.trusted.displaymode.KEY_STICKY";
+  }
+
+  public final class TrustedWebActivityIntent {
+    method public android.content.Intent getIntent();
+    method public void launchTrustedWebActivity(android.content.Context);
+  }
+
+  public class TrustedWebActivityIntentBuilder {
+    ctor public TrustedWebActivityIntentBuilder(android.net.Uri);
+    method public androidx.browser.trusted.TrustedWebActivityIntent build(androidx.browser.customtabs.CustomTabsSession);
+    method public androidx.browser.customtabs.CustomTabsIntent buildCustomTabsIntent();
+    method public androidx.browser.trusted.TrustedWebActivityDisplayMode getDisplayMode();
+    method public android.net.Uri getUri();
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setAdditionalTrustedOrigins(java.util.List<java.lang.String!>);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setColorScheme(int);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setColorSchemeParams(int, androidx.browser.customtabs.CustomTabColorSchemeParams);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setDefaultColorSchemeParams(androidx.browser.customtabs.CustomTabColorSchemeParams);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setDisplayMode(androidx.browser.trusted.TrustedWebActivityDisplayMode);
+    method @Deprecated public androidx.browser.trusted.TrustedWebActivityIntentBuilder setNavigationBarColor(@ColorInt int);
+    method @Deprecated public androidx.browser.trusted.TrustedWebActivityIntentBuilder setNavigationBarDividerColor(@ColorInt int);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setScreenOrientation(int);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setShareParams(androidx.browser.trusted.sharing.ShareTarget, androidx.browser.trusted.sharing.ShareData);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setSplashScreenParams(android.os.Bundle);
+    method @Deprecated public androidx.browser.trusted.TrustedWebActivityIntentBuilder setToolbarColor(@ColorInt int);
+    field public static final String EXTRA_ADDITIONAL_TRUSTED_ORIGINS = "android.support.customtabs.extra.ADDITIONAL_TRUSTED_ORIGINS";
+    field public static final String EXTRA_DISPLAY_MODE = "androidx.browser.trusted.extra.DISPLAY_MODE";
+    field public static final String EXTRA_SCREEN_ORIENTATION = "androidx.browser.trusted.extra.SCREEN_ORIENTATION";
+    field public static final String EXTRA_SHARE_DATA = "androidx.browser.trusted.extra.SHARE_DATA";
+    field public static final String EXTRA_SHARE_TARGET = "androidx.browser.trusted.extra.SHARE_TARGET";
+    field public static final String EXTRA_SPLASH_SCREEN_PARAMS = "androidx.browser.trusted.EXTRA_SPLASH_SCREEN_PARAMS";
+  }
+
+  public abstract class TrustedWebActivityService extends android.app.Service {
+    ctor public TrustedWebActivityService();
+    method @BinderThread public abstract androidx.browser.trusted.TokenStore getTokenStore();
+    method @BinderThread public boolean onAreNotificationsEnabled(String);
+    method @MainThread public final android.os.IBinder? onBind(android.content.Intent?);
+    method @BinderThread public void onCancelNotification(String, int);
+    method @BinderThread public android.os.Bundle? onExtraCommand(String, android.os.Bundle, androidx.browser.trusted.TrustedWebActivityCallbackRemote?);
+    method @BinderThread public android.os.Bundle onGetSmallIconBitmap();
+    method @BinderThread public int onGetSmallIconId();
+    method @BinderThread @RequiresPermission(android.Manifest.permission.POST_NOTIFICATIONS) public boolean onNotifyNotificationWithChannel(String, int, android.app.Notification, String);
+    method @MainThread public final boolean onUnbind(android.content.Intent?);
+    field public static final String ACTION_TRUSTED_WEB_ACTIVITY_SERVICE = "android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE";
+    field public static final String KEY_SMALL_ICON_BITMAP = "android.support.customtabs.trusted.SMALL_ICON_BITMAP";
+    field public static final String KEY_SUCCESS = "androidx.browser.trusted.SUCCESS";
+    field public static final String META_DATA_NAME_SMALL_ICON = "android.support.customtabs.trusted.SMALL_ICON";
+    field public static final int SMALL_ICON_NOT_SET = -1; // 0xffffffff
+  }
+
+  public final class TrustedWebActivityServiceConnection {
+    method public boolean areNotificationsEnabled(String) throws android.os.RemoteException;
+    method public void cancel(String, int) throws android.os.RemoteException;
+    method public android.content.ComponentName getComponentName();
+    method public android.graphics.Bitmap? getSmallIconBitmap() throws android.os.RemoteException;
+    method public int getSmallIconId() throws android.os.RemoteException;
+    method public boolean notify(String, int, android.app.Notification, String) throws android.os.RemoteException;
+    method public android.os.Bundle? sendExtraCommand(String, android.os.Bundle, androidx.browser.trusted.TrustedWebActivityCallback?) throws android.os.RemoteException;
+  }
+
+  public final class TrustedWebActivityServiceConnectionPool {
+    method @MainThread public com.google.common.util.concurrent.ListenableFuture<androidx.browser.trusted.TrustedWebActivityServiceConnection!> connect(android.net.Uri, java.util.Set<androidx.browser.trusted.Token!>, java.util.concurrent.Executor);
+    method public static androidx.browser.trusted.TrustedWebActivityServiceConnectionPool create(android.content.Context);
+    method @MainThread public boolean serviceExistsForScope(android.net.Uri, java.util.Set<androidx.browser.trusted.Token!>);
+  }
+
+}
+
+package androidx.browser.trusted.sharing {
+
+  public final class ShareData {
+    ctor public ShareData(String?, String?, java.util.List<android.net.Uri!>?);
+    method public static androidx.browser.trusted.sharing.ShareData fromBundle(android.os.Bundle);
+    method public android.os.Bundle toBundle();
+    field public static final String KEY_TEXT = "androidx.browser.trusted.sharing.KEY_TEXT";
+    field public static final String KEY_TITLE = "androidx.browser.trusted.sharing.KEY_TITLE";
+    field public static final String KEY_URIS = "androidx.browser.trusted.sharing.KEY_URIS";
+    field public final String? text;
+    field public final String? title;
+    field public final java.util.List<android.net.Uri!>? uris;
+  }
+
+  public final class ShareTarget {
+    ctor public ShareTarget(String, String?, String?, androidx.browser.trusted.sharing.ShareTarget.Params);
+    method public static androidx.browser.trusted.sharing.ShareTarget? fromBundle(android.os.Bundle);
+    method public android.os.Bundle toBundle();
+    field public static final String ENCODING_TYPE_MULTIPART = "multipart/form-data";
+    field public static final String ENCODING_TYPE_URL_ENCODED = "application/x-www-form-urlencoded";
+    field public static final String KEY_ACTION = "androidx.browser.trusted.sharing.KEY_ACTION";
+    field public static final String KEY_ENCTYPE = "androidx.browser.trusted.sharing.KEY_ENCTYPE";
+    field public static final String KEY_METHOD = "androidx.browser.trusted.sharing.KEY_METHOD";
+    field public static final String KEY_PARAMS = "androidx.browser.trusted.sharing.KEY_PARAMS";
+    field public static final String METHOD_GET = "GET";
+    field public static final String METHOD_POST = "POST";
+    field public final String action;
+    field public final String? encodingType;
+    field public final String? method;
+    field public final androidx.browser.trusted.sharing.ShareTarget.Params params;
+  }
+
+  public static final class ShareTarget.FileFormField {
+    ctor public ShareTarget.FileFormField(String, java.util.List<java.lang.String!>);
+    field public static final String KEY_ACCEPTED_TYPES = "androidx.browser.trusted.sharing.KEY_ACCEPTED_TYPES";
+    field public static final String KEY_NAME = "androidx.browser.trusted.sharing.KEY_FILE_NAME";
+    field public final java.util.List<java.lang.String!> acceptedTypes;
+    field public final String name;
+  }
+
+  public static class ShareTarget.Params {
+    ctor public ShareTarget.Params(String?, String?, java.util.List<androidx.browser.trusted.sharing.ShareTarget.FileFormField!>?);
+    field public static final String KEY_FILES = "androidx.browser.trusted.sharing.KEY_FILES";
+    field public static final String KEY_TEXT = "androidx.browser.trusted.sharing.KEY_TEXT";
+    field public static final String KEY_TITLE = "androidx.browser.trusted.sharing.KEY_TITLE";
+    field public final java.util.List<androidx.browser.trusted.sharing.ShareTarget.FileFormField!>? files;
+    field public final String? text;
+    field public final String? title;
+  }
+
+}
+
+package androidx.browser.trusted.splashscreens {
+
+  public final class SplashScreenParamKey {
+    field public static final String KEY_BACKGROUND_COLOR = "androidx.browser.trusted.trusted.KEY_SPLASH_SCREEN_BACKGROUND_COLOR";
+    field public static final String KEY_FADE_OUT_DURATION_MS = "androidx.browser.trusted.KEY_SPLASH_SCREEN_FADE_OUT_DURATION";
+    field public static final String KEY_IMAGE_TRANSFORMATION_MATRIX = "androidx.browser.trusted.KEY_SPLASH_SCREEN_TRANSFORMATION_MATRIX";
+    field public static final String KEY_SCALE_TYPE = "androidx.browser.trusted.KEY_SPLASH_SCREEN_SCALE_TYPE";
+    field public static final String KEY_VERSION = "androidx.browser.trusted.KEY_SPLASH_SCREEN_VERSION";
+  }
+
+  public final class SplashScreenVersion {
+    field public static final String V1 = "androidx.browser.trusted.category.TrustedWebActivitySplashScreensV1";
+  }
+
+}
+
diff --git a/browser/browser/api/public_plus_experimental_1.5.0-beta02.txt b/browser/browser/api/public_plus_experimental_1.5.0-beta02.txt
new file mode 100644
index 0000000..0598270
--- /dev/null
+++ b/browser/browser/api/public_plus_experimental_1.5.0-beta02.txt
@@ -0,0 +1,469 @@
+// Signature format: 4.0
+package androidx.browser.browseractions {
+
+  @Deprecated public class BrowserActionItem {
+    ctor @Deprecated public BrowserActionItem(String, android.app.PendingIntent, @DrawableRes int);
+    ctor @Deprecated public BrowserActionItem(String, android.app.PendingIntent);
+    method @Deprecated public android.app.PendingIntent getAction();
+    method @Deprecated public int getIconId();
+    method @Deprecated public String getTitle();
+  }
+
+  @Deprecated public class BrowserActionsIntent {
+    method @Deprecated public static String? getCreatorPackageName(android.content.Intent);
+    method @Deprecated public android.content.Intent getIntent();
+    method @Deprecated public static String? getUntrustedCreatorPackageName(android.content.Intent);
+    method @Deprecated public static void launchIntent(android.content.Context, android.content.Intent);
+    method @Deprecated public static void openBrowserAction(android.content.Context, android.net.Uri);
+    method @Deprecated public static void openBrowserAction(android.content.Context, android.net.Uri, int, java.util.ArrayList<androidx.browser.browseractions.BrowserActionItem!>, android.app.PendingIntent);
+    method @Deprecated public static java.util.List<androidx.browser.browseractions.BrowserActionItem!> parseBrowserActionItems(java.util.ArrayList<android.os.Bundle!>);
+    field @Deprecated public static final String ACTION_BROWSER_ACTIONS_OPEN = "androidx.browser.browseractions.browser_action_open";
+    field @Deprecated public static final String EXTRA_APP_ID = "androidx.browser.browseractions.APP_ID";
+    field @Deprecated public static final String EXTRA_MENU_ITEMS = "androidx.browser.browseractions.extra.MENU_ITEMS";
+    field @Deprecated public static final String EXTRA_SELECTED_ACTION_PENDING_INTENT = "androidx.browser.browseractions.extra.SELECTED_ACTION_PENDING_INTENT";
+    field @Deprecated public static final String EXTRA_TYPE = "androidx.browser.browseractions.extra.TYPE";
+    field @Deprecated public static final int ITEM_COPY = 3; // 0x3
+    field @Deprecated public static final int ITEM_DOWNLOAD = 2; // 0x2
+    field @Deprecated public static final int ITEM_INVALID_ITEM = -1; // 0xffffffff
+    field @Deprecated public static final int ITEM_OPEN_IN_INCOGNITO = 1; // 0x1
+    field @Deprecated public static final int ITEM_OPEN_IN_NEW_TAB = 0; // 0x0
+    field @Deprecated public static final int ITEM_SHARE = 4; // 0x4
+    field @Deprecated public static final String KEY_ACTION = "androidx.browser.browseractions.ACTION";
+    field @Deprecated public static final String KEY_ICON_ID = "androidx.browser.browseractions.ICON_ID";
+    field @Deprecated public static final String KEY_TITLE = "androidx.browser.browseractions.TITLE";
+    field @Deprecated public static final int MAX_CUSTOM_ITEMS = 5; // 0x5
+    field @Deprecated public static final int URL_TYPE_AUDIO = 3; // 0x3
+    field @Deprecated public static final int URL_TYPE_FILE = 4; // 0x4
+    field @Deprecated public static final int URL_TYPE_IMAGE = 1; // 0x1
+    field @Deprecated public static final int URL_TYPE_NONE = 0; // 0x0
+    field @Deprecated public static final int URL_TYPE_PLUGIN = 5; // 0x5
+    field @Deprecated public static final int URL_TYPE_VIDEO = 2; // 0x2
+  }
+
+  @Deprecated public static final class BrowserActionsIntent.Builder {
+    ctor @Deprecated public BrowserActionsIntent.Builder(android.content.Context, android.net.Uri);
+    method @Deprecated public androidx.browser.browseractions.BrowserActionsIntent build();
+    method @Deprecated public androidx.browser.browseractions.BrowserActionsIntent.Builder setCustomItems(java.util.ArrayList<androidx.browser.browseractions.BrowserActionItem!>);
+    method @Deprecated public androidx.browser.browseractions.BrowserActionsIntent.Builder setCustomItems(androidx.browser.browseractions.BrowserActionItem!...);
+    method @Deprecated public androidx.browser.browseractions.BrowserActionsIntent.Builder setOnItemSelectedAction(android.app.PendingIntent);
+    method @Deprecated public androidx.browser.browseractions.BrowserActionsIntent.Builder setUrlType(int);
+  }
+
+}
+
+package androidx.browser.customtabs {
+
+  public final class CustomTabColorSchemeParams {
+    field @ColorInt public final Integer? navigationBarColor;
+    field @ColorInt public final Integer? navigationBarDividerColor;
+    field @ColorInt public final Integer? secondaryToolbarColor;
+    field @ColorInt public final Integer? toolbarColor;
+  }
+
+  public static final class CustomTabColorSchemeParams.Builder {
+    ctor public CustomTabColorSchemeParams.Builder();
+    method public androidx.browser.customtabs.CustomTabColorSchemeParams build();
+    method public androidx.browser.customtabs.CustomTabColorSchemeParams.Builder setNavigationBarColor(@ColorInt int);
+    method public androidx.browser.customtabs.CustomTabColorSchemeParams.Builder setNavigationBarDividerColor(@ColorInt int);
+    method public androidx.browser.customtabs.CustomTabColorSchemeParams.Builder setSecondaryToolbarColor(@ColorInt int);
+    method public androidx.browser.customtabs.CustomTabColorSchemeParams.Builder setToolbarColor(@ColorInt int);
+  }
+
+  public class CustomTabsCallback {
+    ctor public CustomTabsCallback();
+    method public void extraCallback(String, android.os.Bundle?);
+    method public android.os.Bundle? extraCallbackWithResult(String, android.os.Bundle?);
+    method public void onActivityResized(@Dimension(unit=androidx.annotation.Dimension.PX) int, @Dimension(unit=androidx.annotation.Dimension.PX) int, android.os.Bundle);
+    method public void onMessageChannelReady(android.os.Bundle?);
+    method public void onNavigationEvent(int, android.os.Bundle?);
+    method public void onPostMessage(String, android.os.Bundle?);
+    method public void onRelationshipValidationResult(@androidx.browser.customtabs.CustomTabsService.Relation int, android.net.Uri, boolean, android.os.Bundle?);
+    field public static final int NAVIGATION_ABORTED = 4; // 0x4
+    field public static final int NAVIGATION_FAILED = 3; // 0x3
+    field public static final int NAVIGATION_FINISHED = 2; // 0x2
+    field public static final int NAVIGATION_STARTED = 1; // 0x1
+    field public static final int TAB_HIDDEN = 6; // 0x6
+    field public static final int TAB_SHOWN = 5; // 0x5
+  }
+
+  public class CustomTabsClient {
+    method public static boolean bindCustomTabsService(android.content.Context, String?, androidx.browser.customtabs.CustomTabsServiceConnection);
+    method public static boolean bindCustomTabsServicePreservePriority(android.content.Context, String?, androidx.browser.customtabs.CustomTabsServiceConnection);
+    method public static boolean connectAndInitialize(android.content.Context, String);
+    method public android.os.Bundle? extraCommand(String, android.os.Bundle?);
+    method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?);
+    method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?, boolean);
+    method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?);
+    method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?, int);
+    method public boolean warmup(long);
+  }
+
+  public final class CustomTabsIntent {
+    method public static int getActivityResizeBehavior(android.content.Intent);
+    method public static int getCloseButtonPosition(android.content.Intent);
+    method public static androidx.browser.customtabs.CustomTabColorSchemeParams getColorSchemeParams(android.content.Intent, int);
+    method @Dimension(unit=androidx.annotation.Dimension.PX) public static int getInitialActivityHeightPx(android.content.Intent);
+    method public static int getMaxToolbarItems();
+    method @Dimension(unit=androidx.annotation.Dimension.DP) public static int getToolbarCornerRadiusDp(android.content.Intent);
+    method public void launchUrl(android.content.Context, android.net.Uri);
+    method public static android.content.Intent setAlwaysUseBrowserUI(android.content.Intent?);
+    method public static boolean shouldAlwaysUseBrowserUI(android.content.Intent);
+    field public static final int ACTIVITY_HEIGHT_ADJUSTABLE = 1; // 0x1
+    field public static final int ACTIVITY_HEIGHT_DEFAULT = 0; // 0x0
+    field public static final int ACTIVITY_HEIGHT_FIXED = 2; // 0x2
+    field public static final int CLOSE_BUTTON_POSITION_DEFAULT = 0; // 0x0
+    field public static final int CLOSE_BUTTON_POSITION_END = 2; // 0x2
+    field public static final int CLOSE_BUTTON_POSITION_START = 1; // 0x1
+    field public static final int COLOR_SCHEME_DARK = 2; // 0x2
+    field public static final int COLOR_SCHEME_LIGHT = 1; // 0x1
+    field public static final int COLOR_SCHEME_SYSTEM = 0; // 0x0
+    field public static final String EXTRA_ACTION_BUTTON_BUNDLE = "android.support.customtabs.extra.ACTION_BUTTON_BUNDLE";
+    field public static final String EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR = "androidx.browser.customtabs.extra.ACTIVITY_HEIGHT_RESIZE_BEHAVIOR";
+    field public static final String EXTRA_CLOSE_BUTTON_ICON = "android.support.customtabs.extra.CLOSE_BUTTON_ICON";
+    field public static final String EXTRA_CLOSE_BUTTON_POSITION = "androidx.browser.customtabs.extra.CLOSE_BUTTON_POSITION";
+    field public static final String EXTRA_COLOR_SCHEME = "androidx.browser.customtabs.extra.COLOR_SCHEME";
+    field public static final String EXTRA_COLOR_SCHEME_PARAMS = "androidx.browser.customtabs.extra.COLOR_SCHEME_PARAMS";
+    field @Deprecated public static final String EXTRA_DEFAULT_SHARE_MENU_ITEM = "android.support.customtabs.extra.SHARE_MENU_ITEM";
+    field public static final String EXTRA_ENABLE_INSTANT_APPS = "android.support.customtabs.extra.EXTRA_ENABLE_INSTANT_APPS";
+    field public static final String EXTRA_ENABLE_URLBAR_HIDING = "android.support.customtabs.extra.ENABLE_URLBAR_HIDING";
+    field public static final String EXTRA_EXIT_ANIMATION_BUNDLE = "android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE";
+    field public static final String EXTRA_INITIAL_ACTIVITY_HEIGHT_PX = "androidx.browser.customtabs.extra.INITIAL_ACTIVITY_HEIGHT_PX";
+    field public static final String EXTRA_MENU_ITEMS = "android.support.customtabs.extra.MENU_ITEMS";
+    field public static final String EXTRA_NAVIGATION_BAR_COLOR = "androidx.browser.customtabs.extra.NAVIGATION_BAR_COLOR";
+    field public static final String EXTRA_NAVIGATION_BAR_DIVIDER_COLOR = "androidx.browser.customtabs.extra.NAVIGATION_BAR_DIVIDER_COLOR";
+    field public static final String EXTRA_REMOTEVIEWS = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS";
+    field public static final String EXTRA_REMOTEVIEWS_CLICKED_ID = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_CLICKED_ID";
+    field public static final String EXTRA_REMOTEVIEWS_PENDINGINTENT = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_PENDINGINTENT";
+    field public static final String EXTRA_REMOTEVIEWS_VIEW_IDS = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_VIEW_IDS";
+    field public static final String EXTRA_SECONDARY_TOOLBAR_COLOR = "android.support.customtabs.extra.SECONDARY_TOOLBAR_COLOR";
+    field public static final String EXTRA_SESSION = "android.support.customtabs.extra.SESSION";
+    field public static final String EXTRA_SHARE_STATE = "androidx.browser.customtabs.extra.SHARE_STATE";
+    field public static final String EXTRA_TINT_ACTION_BUTTON = "android.support.customtabs.extra.TINT_ACTION_BUTTON";
+    field public static final String EXTRA_TITLE_VISIBILITY_STATE = "android.support.customtabs.extra.TITLE_VISIBILITY";
+    field public static final String EXTRA_TOOLBAR_COLOR = "android.support.customtabs.extra.TOOLBAR_COLOR";
+    field public static final String EXTRA_TOOLBAR_CORNER_RADIUS_DP = "androidx.browser.customtabs.extra.TOOLBAR_CORNER_RADIUS_DP";
+    field public static final String EXTRA_TOOLBAR_ITEMS = "android.support.customtabs.extra.TOOLBAR_ITEMS";
+    field public static final String KEY_DESCRIPTION = "android.support.customtabs.customaction.DESCRIPTION";
+    field public static final String KEY_ICON = "android.support.customtabs.customaction.ICON";
+    field public static final String KEY_ID = "android.support.customtabs.customaction.ID";
+    field public static final String KEY_MENU_ITEM_TITLE = "android.support.customtabs.customaction.MENU_ITEM_TITLE";
+    field public static final String KEY_PENDING_INTENT = "android.support.customtabs.customaction.PENDING_INTENT";
+    field public static final int NO_TITLE = 0; // 0x0
+    field public static final int SHARE_STATE_DEFAULT = 0; // 0x0
+    field public static final int SHARE_STATE_OFF = 2; // 0x2
+    field public static final int SHARE_STATE_ON = 1; // 0x1
+    field public static final int SHOW_PAGE_TITLE = 1; // 0x1
+    field public static final int TOOLBAR_ACTION_BUTTON_ID = 0; // 0x0
+    field public final android.content.Intent intent;
+    field public final android.os.Bundle? startAnimationBundle;
+  }
+
+  public static final class CustomTabsIntent.Builder {
+    ctor public CustomTabsIntent.Builder();
+    ctor public CustomTabsIntent.Builder(androidx.browser.customtabs.CustomTabsSession?);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder addDefaultShareMenuItem();
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder addMenuItem(String, android.app.PendingIntent);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder addToolbarItem(int, android.graphics.Bitmap, String, android.app.PendingIntent) throws java.lang.IllegalStateException;
+    method public androidx.browser.customtabs.CustomTabsIntent build();
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder enableUrlBarHiding();
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setActionButton(android.graphics.Bitmap, String, android.app.PendingIntent, boolean);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setActionButton(android.graphics.Bitmap, String, android.app.PendingIntent);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setCloseButtonIcon(android.graphics.Bitmap);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setCloseButtonPosition(int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setColorScheme(int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setColorSchemeParams(int, androidx.browser.customtabs.CustomTabColorSchemeParams);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setDefaultColorSchemeParams(androidx.browser.customtabs.CustomTabColorSchemeParams);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setDefaultShareMenuItemEnabled(boolean);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setExitAnimations(android.content.Context, @AnimRes int, @AnimRes int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setInitialActivityHeightPx(@Dimension(unit=androidx.annotation.Dimension.PX) int, int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setInitialActivityHeightPx(@Dimension(unit=androidx.annotation.Dimension.PX) int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setInstantAppsEnabled(boolean);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setNavigationBarColor(@ColorInt int);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setNavigationBarDividerColor(@ColorInt int);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarColor(@ColorInt int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarViews(android.widget.RemoteViews, int[]?, android.app.PendingIntent?);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setSession(androidx.browser.customtabs.CustomTabsSession);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setShareState(int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setShowTitle(boolean);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setStartAnimations(android.content.Context, @AnimRes int, @AnimRes int);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setToolbarColor(@ColorInt int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setToolbarCornerRadiusDp(@Dimension(unit=androidx.annotation.Dimension.DP) int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setUrlBarHidingEnabled(boolean);
+  }
+
+  public abstract class CustomTabsService extends android.app.Service {
+    ctor public CustomTabsService();
+    method protected boolean cleanUpSession(androidx.browser.customtabs.CustomTabsSessionToken);
+    method protected abstract android.os.Bundle? extraCommand(String, android.os.Bundle?);
+    method protected abstract boolean mayLaunchUrl(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri?, android.os.Bundle?, java.util.List<android.os.Bundle!>?);
+    method protected abstract boolean newSession(androidx.browser.customtabs.CustomTabsSessionToken);
+    method public android.os.IBinder onBind(android.content.Intent?);
+    method @androidx.browser.customtabs.CustomTabsService.Result protected abstract int postMessage(androidx.browser.customtabs.CustomTabsSessionToken, String, android.os.Bundle?);
+    method protected abstract boolean receiveFile(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri, int, android.os.Bundle?);
+    method protected abstract boolean requestPostMessageChannel(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri);
+    method protected abstract boolean updateVisuals(androidx.browser.customtabs.CustomTabsSessionToken, android.os.Bundle?);
+    method protected abstract boolean validateRelationship(androidx.browser.customtabs.CustomTabsSessionToken, @androidx.browser.customtabs.CustomTabsService.Relation int, android.net.Uri, android.os.Bundle?);
+    method protected abstract boolean warmup(long);
+    field public static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService";
+    field public static final String CATEGORY_COLOR_SCHEME_CUSTOMIZATION = "androidx.browser.customtabs.category.ColorSchemeCustomization";
+    field public static final String CATEGORY_NAVBAR_COLOR_CUSTOMIZATION = "androidx.browser.customtabs.category.NavBarColorCustomization";
+    field public static final String CATEGORY_TRUSTED_WEB_ACTIVITY_IMMERSIVE_MODE = "androidx.browser.trusted.category.ImmersiveMode";
+    field public static final String CATEGORY_WEB_SHARE_TARGET_V2 = "androidx.browser.trusted.category.WebShareTargetV2";
+    field public static final int FILE_PURPOSE_TRUSTED_WEB_ACTIVITY_SPLASH_IMAGE = 1; // 0x1
+    field public static final String KEY_SUCCESS = "androidx.browser.customtabs.SUCCESS";
+    field public static final String KEY_URL = "android.support.customtabs.otherurls.URL";
+    field public static final int RELATION_HANDLE_ALL_URLS = 2; // 0x2
+    field public static final int RELATION_USE_AS_ORIGIN = 1; // 0x1
+    field public static final int RESULT_FAILURE_DISALLOWED = -1; // 0xffffffff
+    field public static final int RESULT_FAILURE_MESSAGING_ERROR = -3; // 0xfffffffd
+    field public static final int RESULT_FAILURE_REMOTE_ERROR = -2; // 0xfffffffe
+    field public static final int RESULT_SUCCESS = 0; // 0x0
+    field public static final String TRUSTED_WEB_ACTIVITY_CATEGORY = "androidx.browser.trusted.category.TrustedWebActivities";
+  }
+
+  @IntDef({androidx.browser.customtabs.CustomTabsService.RELATION_USE_AS_ORIGIN, androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface CustomTabsService.Relation {
+  }
+
+  @IntDef({androidx.browser.customtabs.CustomTabsService.RESULT_SUCCESS, androidx.browser.customtabs.CustomTabsService.RESULT_FAILURE_DISALLOWED, androidx.browser.customtabs.CustomTabsService.RESULT_FAILURE_REMOTE_ERROR, androidx.browser.customtabs.CustomTabsService.RESULT_FAILURE_MESSAGING_ERROR}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface CustomTabsService.Result {
+  }
+
+  public abstract class CustomTabsServiceConnection implements android.content.ServiceConnection {
+    ctor public CustomTabsServiceConnection();
+    method public abstract void onCustomTabsServiceConnected(android.content.ComponentName, androidx.browser.customtabs.CustomTabsClient);
+    method public final void onServiceConnected(android.content.ComponentName, android.os.IBinder);
+  }
+
+  public final class CustomTabsSession {
+    method @VisibleForTesting public static androidx.browser.customtabs.CustomTabsSession createMockSessionForTesting(android.content.ComponentName);
+    method public boolean mayLaunchUrl(android.net.Uri?, android.os.Bundle?, java.util.List<android.os.Bundle!>?);
+    method @androidx.browser.customtabs.CustomTabsService.Result public int postMessage(String, android.os.Bundle?);
+    method public boolean receiveFile(android.net.Uri, int, android.os.Bundle?);
+    method public boolean requestPostMessageChannel(android.net.Uri);
+    method public boolean setActionButton(android.graphics.Bitmap, String);
+    method public boolean setSecondaryToolbarViews(android.widget.RemoteViews?, int[]?, android.app.PendingIntent?);
+    method @Deprecated public boolean setToolbarItem(int, android.graphics.Bitmap, String);
+    method public boolean validateRelationship(@androidx.browser.customtabs.CustomTabsService.Relation int, android.net.Uri, android.os.Bundle?);
+  }
+
+  public class CustomTabsSessionToken {
+    method public static androidx.browser.customtabs.CustomTabsSessionToken createMockSessionTokenForTesting();
+    method public androidx.browser.customtabs.CustomTabsCallback? getCallback();
+    method public static androidx.browser.customtabs.CustomTabsSessionToken? getSessionTokenFromIntent(android.content.Intent);
+    method public boolean isAssociatedWith(androidx.browser.customtabs.CustomTabsSession);
+  }
+
+  public class PostMessageService extends android.app.Service {
+    ctor public PostMessageService();
+    method public android.os.IBinder onBind(android.content.Intent?);
+  }
+
+  public abstract class PostMessageServiceConnection implements android.content.ServiceConnection {
+    ctor public PostMessageServiceConnection(androidx.browser.customtabs.CustomTabsSessionToken);
+    method public boolean bindSessionToPostMessageService(android.content.Context, String);
+    method public final boolean notifyMessageChannelReady(android.os.Bundle?);
+    method public void onPostMessageServiceConnected();
+    method public void onPostMessageServiceDisconnected();
+    method public final void onServiceConnected(android.content.ComponentName, android.os.IBinder);
+    method public final void onServiceDisconnected(android.content.ComponentName);
+    method public final boolean postMessage(String, android.os.Bundle?);
+    method public void unbindFromContext(android.content.Context);
+  }
+
+  public class TrustedWebUtils {
+    method public static boolean areSplashScreensSupported(android.content.Context, String, String);
+    method @Deprecated public static void launchAsTrustedWebActivity(android.content.Context, androidx.browser.customtabs.CustomTabsIntent, android.net.Uri);
+    method @WorkerThread public static boolean transferSplashImage(android.content.Context, java.io.File, String, String, androidx.browser.customtabs.CustomTabsSession);
+    field public static final String EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY = "android.support.customtabs.extra.LAUNCH_AS_TRUSTED_WEB_ACTIVITY";
+  }
+
+}
+
+package androidx.browser.trusted {
+
+  public final class ScreenOrientation {
+    field public static final int ANY = 5; // 0x5
+    field public static final int DEFAULT = 0; // 0x0
+    field public static final int LANDSCAPE = 6; // 0x6
+    field public static final int LANDSCAPE_PRIMARY = 3; // 0x3
+    field public static final int LANDSCAPE_SECONDARY = 4; // 0x4
+    field public static final int NATURAL = 8; // 0x8
+    field public static final int PORTRAIT = 7; // 0x7
+    field public static final int PORTRAIT_PRIMARY = 1; // 0x1
+    field public static final int PORTRAIT_SECONDARY = 2; // 0x2
+  }
+
+  public final class Token {
+    method public static androidx.browser.trusted.Token? create(String, android.content.pm.PackageManager);
+    method public static androidx.browser.trusted.Token deserialize(byte[]);
+    method public boolean matches(String, android.content.pm.PackageManager);
+    method public byte[] serialize();
+  }
+
+  public interface TokenStore {
+    method @BinderThread public androidx.browser.trusted.Token? load();
+    method @WorkerThread public void store(androidx.browser.trusted.Token?);
+  }
+
+  public abstract class TrustedWebActivityCallback {
+    ctor public TrustedWebActivityCallback();
+    method public abstract void onExtraCallback(String, android.os.Bundle?);
+  }
+
+  public class TrustedWebActivityCallbackRemote {
+    method public void runExtraCallback(String, android.os.Bundle) throws android.os.RemoteException;
+  }
+
+  public interface TrustedWebActivityDisplayMode {
+    method public static androidx.browser.trusted.TrustedWebActivityDisplayMode fromBundle(android.os.Bundle);
+    method public android.os.Bundle toBundle();
+    field public static final String KEY_ID = "androidx.browser.trusted.displaymode.KEY_ID";
+  }
+
+  public static class TrustedWebActivityDisplayMode.DefaultMode implements androidx.browser.trusted.TrustedWebActivityDisplayMode {
+    ctor public TrustedWebActivityDisplayMode.DefaultMode();
+    method public android.os.Bundle toBundle();
+  }
+
+  public static class TrustedWebActivityDisplayMode.ImmersiveMode implements androidx.browser.trusted.TrustedWebActivityDisplayMode {
+    ctor public TrustedWebActivityDisplayMode.ImmersiveMode(boolean, int);
+    method public boolean isSticky();
+    method public int layoutInDisplayCutoutMode();
+    method public android.os.Bundle toBundle();
+    field public static final String KEY_CUTOUT_MODE = "androidx.browser.trusted.displaymode.KEY_CUTOUT_MODE";
+    field public static final String KEY_STICKY = "androidx.browser.trusted.displaymode.KEY_STICKY";
+  }
+
+  public final class TrustedWebActivityIntent {
+    method public android.content.Intent getIntent();
+    method public void launchTrustedWebActivity(android.content.Context);
+  }
+
+  public class TrustedWebActivityIntentBuilder {
+    ctor public TrustedWebActivityIntentBuilder(android.net.Uri);
+    method public androidx.browser.trusted.TrustedWebActivityIntent build(androidx.browser.customtabs.CustomTabsSession);
+    method public androidx.browser.customtabs.CustomTabsIntent buildCustomTabsIntent();
+    method public androidx.browser.trusted.TrustedWebActivityDisplayMode getDisplayMode();
+    method public android.net.Uri getUri();
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setAdditionalTrustedOrigins(java.util.List<java.lang.String!>);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setColorScheme(int);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setColorSchemeParams(int, androidx.browser.customtabs.CustomTabColorSchemeParams);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setDefaultColorSchemeParams(androidx.browser.customtabs.CustomTabColorSchemeParams);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setDisplayMode(androidx.browser.trusted.TrustedWebActivityDisplayMode);
+    method @Deprecated public androidx.browser.trusted.TrustedWebActivityIntentBuilder setNavigationBarColor(@ColorInt int);
+    method @Deprecated public androidx.browser.trusted.TrustedWebActivityIntentBuilder setNavigationBarDividerColor(@ColorInt int);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setScreenOrientation(int);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setShareParams(androidx.browser.trusted.sharing.ShareTarget, androidx.browser.trusted.sharing.ShareData);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setSplashScreenParams(android.os.Bundle);
+    method @Deprecated public androidx.browser.trusted.TrustedWebActivityIntentBuilder setToolbarColor(@ColorInt int);
+    field public static final String EXTRA_ADDITIONAL_TRUSTED_ORIGINS = "android.support.customtabs.extra.ADDITIONAL_TRUSTED_ORIGINS";
+    field public static final String EXTRA_DISPLAY_MODE = "androidx.browser.trusted.extra.DISPLAY_MODE";
+    field public static final String EXTRA_SCREEN_ORIENTATION = "androidx.browser.trusted.extra.SCREEN_ORIENTATION";
+    field public static final String EXTRA_SHARE_DATA = "androidx.browser.trusted.extra.SHARE_DATA";
+    field public static final String EXTRA_SHARE_TARGET = "androidx.browser.trusted.extra.SHARE_TARGET";
+    field public static final String EXTRA_SPLASH_SCREEN_PARAMS = "androidx.browser.trusted.EXTRA_SPLASH_SCREEN_PARAMS";
+  }
+
+  public abstract class TrustedWebActivityService extends android.app.Service {
+    ctor public TrustedWebActivityService();
+    method @BinderThread public abstract androidx.browser.trusted.TokenStore getTokenStore();
+    method @BinderThread public boolean onAreNotificationsEnabled(String);
+    method @MainThread public final android.os.IBinder? onBind(android.content.Intent?);
+    method @BinderThread public void onCancelNotification(String, int);
+    method @BinderThread public android.os.Bundle? onExtraCommand(String, android.os.Bundle, androidx.browser.trusted.TrustedWebActivityCallbackRemote?);
+    method @BinderThread public android.os.Bundle onGetSmallIconBitmap();
+    method @BinderThread public int onGetSmallIconId();
+    method @BinderThread @RequiresPermission(android.Manifest.permission.POST_NOTIFICATIONS) public boolean onNotifyNotificationWithChannel(String, int, android.app.Notification, String);
+    method @MainThread public final boolean onUnbind(android.content.Intent?);
+    field public static final String ACTION_TRUSTED_WEB_ACTIVITY_SERVICE = "android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE";
+    field public static final String KEY_SMALL_ICON_BITMAP = "android.support.customtabs.trusted.SMALL_ICON_BITMAP";
+    field public static final String KEY_SUCCESS = "androidx.browser.trusted.SUCCESS";
+    field public static final String META_DATA_NAME_SMALL_ICON = "android.support.customtabs.trusted.SMALL_ICON";
+    field public static final int SMALL_ICON_NOT_SET = -1; // 0xffffffff
+  }
+
+  public final class TrustedWebActivityServiceConnection {
+    method public boolean areNotificationsEnabled(String) throws android.os.RemoteException;
+    method public void cancel(String, int) throws android.os.RemoteException;
+    method public android.content.ComponentName getComponentName();
+    method public android.graphics.Bitmap? getSmallIconBitmap() throws android.os.RemoteException;
+    method public int getSmallIconId() throws android.os.RemoteException;
+    method public boolean notify(String, int, android.app.Notification, String) throws android.os.RemoteException;
+    method public android.os.Bundle? sendExtraCommand(String, android.os.Bundle, androidx.browser.trusted.TrustedWebActivityCallback?) throws android.os.RemoteException;
+  }
+
+  public final class TrustedWebActivityServiceConnectionPool {
+    method @MainThread public com.google.common.util.concurrent.ListenableFuture<androidx.browser.trusted.TrustedWebActivityServiceConnection!> connect(android.net.Uri, java.util.Set<androidx.browser.trusted.Token!>, java.util.concurrent.Executor);
+    method public static androidx.browser.trusted.TrustedWebActivityServiceConnectionPool create(android.content.Context);
+    method @MainThread public boolean serviceExistsForScope(android.net.Uri, java.util.Set<androidx.browser.trusted.Token!>);
+  }
+
+}
+
+package androidx.browser.trusted.sharing {
+
+  public final class ShareData {
+    ctor public ShareData(String?, String?, java.util.List<android.net.Uri!>?);
+    method public static androidx.browser.trusted.sharing.ShareData fromBundle(android.os.Bundle);
+    method public android.os.Bundle toBundle();
+    field public static final String KEY_TEXT = "androidx.browser.trusted.sharing.KEY_TEXT";
+    field public static final String KEY_TITLE = "androidx.browser.trusted.sharing.KEY_TITLE";
+    field public static final String KEY_URIS = "androidx.browser.trusted.sharing.KEY_URIS";
+    field public final String? text;
+    field public final String? title;
+    field public final java.util.List<android.net.Uri!>? uris;
+  }
+
+  public final class ShareTarget {
+    ctor public ShareTarget(String, String?, String?, androidx.browser.trusted.sharing.ShareTarget.Params);
+    method public static androidx.browser.trusted.sharing.ShareTarget? fromBundle(android.os.Bundle);
+    method public android.os.Bundle toBundle();
+    field public static final String ENCODING_TYPE_MULTIPART = "multipart/form-data";
+    field public static final String ENCODING_TYPE_URL_ENCODED = "application/x-www-form-urlencoded";
+    field public static final String KEY_ACTION = "androidx.browser.trusted.sharing.KEY_ACTION";
+    field public static final String KEY_ENCTYPE = "androidx.browser.trusted.sharing.KEY_ENCTYPE";
+    field public static final String KEY_METHOD = "androidx.browser.trusted.sharing.KEY_METHOD";
+    field public static final String KEY_PARAMS = "androidx.browser.trusted.sharing.KEY_PARAMS";
+    field public static final String METHOD_GET = "GET";
+    field public static final String METHOD_POST = "POST";
+    field public final String action;
+    field public final String? encodingType;
+    field public final String? method;
+    field public final androidx.browser.trusted.sharing.ShareTarget.Params params;
+  }
+
+  public static final class ShareTarget.FileFormField {
+    ctor public ShareTarget.FileFormField(String, java.util.List<java.lang.String!>);
+    field public static final String KEY_ACCEPTED_TYPES = "androidx.browser.trusted.sharing.KEY_ACCEPTED_TYPES";
+    field public static final String KEY_NAME = "androidx.browser.trusted.sharing.KEY_FILE_NAME";
+    field public final java.util.List<java.lang.String!> acceptedTypes;
+    field public final String name;
+  }
+
+  public static class ShareTarget.Params {
+    ctor public ShareTarget.Params(String?, String?, java.util.List<androidx.browser.trusted.sharing.ShareTarget.FileFormField!>?);
+    field public static final String KEY_FILES = "androidx.browser.trusted.sharing.KEY_FILES";
+    field public static final String KEY_TEXT = "androidx.browser.trusted.sharing.KEY_TEXT";
+    field public static final String KEY_TITLE = "androidx.browser.trusted.sharing.KEY_TITLE";
+    field public final java.util.List<androidx.browser.trusted.sharing.ShareTarget.FileFormField!>? files;
+    field public final String? text;
+    field public final String? title;
+  }
+
+}
+
+package androidx.browser.trusted.splashscreens {
+
+  public final class SplashScreenParamKey {
+    field public static final String KEY_BACKGROUND_COLOR = "androidx.browser.trusted.trusted.KEY_SPLASH_SCREEN_BACKGROUND_COLOR";
+    field public static final String KEY_FADE_OUT_DURATION_MS = "androidx.browser.trusted.KEY_SPLASH_SCREEN_FADE_OUT_DURATION";
+    field public static final String KEY_IMAGE_TRANSFORMATION_MATRIX = "androidx.browser.trusted.KEY_SPLASH_SCREEN_TRANSFORMATION_MATRIX";
+    field public static final String KEY_SCALE_TYPE = "androidx.browser.trusted.KEY_SPLASH_SCREEN_SCALE_TYPE";
+    field public static final String KEY_VERSION = "androidx.browser.trusted.KEY_SPLASH_SCREEN_VERSION";
+  }
+
+  public final class SplashScreenVersion {
+    field public static final String V1 = "androidx.browser.trusted.category.TrustedWebActivitySplashScreensV1";
+  }
+
+}
+
diff --git a/browser/browser/api/res-1.5.0-beta02.txt b/browser/browser/api/res-1.5.0-beta02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/browser/browser/api/res-1.5.0-beta02.txt
diff --git a/browser/browser/api/restricted_1.5.0-beta02.txt b/browser/browser/api/restricted_1.5.0-beta02.txt
new file mode 100644
index 0000000..de4bb0c
--- /dev/null
+++ b/browser/browser/api/restricted_1.5.0-beta02.txt
@@ -0,0 +1,480 @@
+// Signature format: 4.0
+package androidx.browser.browseractions {
+
+  @Deprecated public class BrowserActionItem {
+    ctor @Deprecated public BrowserActionItem(String, android.app.PendingIntent, @DrawableRes int);
+    ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public BrowserActionItem(String, android.app.PendingIntent, android.net.Uri);
+    ctor @Deprecated public BrowserActionItem(String, android.app.PendingIntent);
+    method @Deprecated public android.app.PendingIntent getAction();
+    method @Deprecated public int getIconId();
+    method @Deprecated public String getTitle();
+  }
+
+  @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class BrowserActionsFallbackMenuView extends android.widget.LinearLayout {
+    ctor @Deprecated public BrowserActionsFallbackMenuView(android.content.Context, android.util.AttributeSet);
+  }
+
+  @Deprecated public class BrowserActionsIntent {
+    method @Deprecated public static String? getCreatorPackageName(android.content.Intent);
+    method @Deprecated public android.content.Intent getIntent();
+    method @Deprecated public static String? getUntrustedCreatorPackageName(android.content.Intent);
+    method @Deprecated public static void launchIntent(android.content.Context, android.content.Intent);
+    method @Deprecated public static void openBrowserAction(android.content.Context, android.net.Uri);
+    method @Deprecated public static void openBrowserAction(android.content.Context, android.net.Uri, int, java.util.ArrayList<androidx.browser.browseractions.BrowserActionItem!>, android.app.PendingIntent);
+    method @Deprecated public static java.util.List<androidx.browser.browseractions.BrowserActionItem!> parseBrowserActionItems(java.util.ArrayList<android.os.Bundle!>);
+    field @Deprecated public static final String ACTION_BROWSER_ACTIONS_OPEN = "androidx.browser.browseractions.browser_action_open";
+    field @Deprecated public static final String EXTRA_APP_ID = "androidx.browser.browseractions.APP_ID";
+    field @Deprecated public static final String EXTRA_MENU_ITEMS = "androidx.browser.browseractions.extra.MENU_ITEMS";
+    field @Deprecated public static final String EXTRA_SELECTED_ACTION_PENDING_INTENT = "androidx.browser.browseractions.extra.SELECTED_ACTION_PENDING_INTENT";
+    field @Deprecated public static final String EXTRA_TYPE = "androidx.browser.browseractions.extra.TYPE";
+    field @Deprecated public static final int ITEM_COPY = 3; // 0x3
+    field @Deprecated public static final int ITEM_DOWNLOAD = 2; // 0x2
+    field @Deprecated public static final int ITEM_INVALID_ITEM = -1; // 0xffffffff
+    field @Deprecated public static final int ITEM_OPEN_IN_INCOGNITO = 1; // 0x1
+    field @Deprecated public static final int ITEM_OPEN_IN_NEW_TAB = 0; // 0x0
+    field @Deprecated public static final int ITEM_SHARE = 4; // 0x4
+    field @Deprecated public static final String KEY_ACTION = "androidx.browser.browseractions.ACTION";
+    field @Deprecated public static final String KEY_ICON_ID = "androidx.browser.browseractions.ICON_ID";
+    field @Deprecated public static final String KEY_TITLE = "androidx.browser.browseractions.TITLE";
+    field @Deprecated public static final int MAX_CUSTOM_ITEMS = 5; // 0x5
+    field @Deprecated public static final int URL_TYPE_AUDIO = 3; // 0x3
+    field @Deprecated public static final int URL_TYPE_FILE = 4; // 0x4
+    field @Deprecated public static final int URL_TYPE_IMAGE = 1; // 0x1
+    field @Deprecated public static final int URL_TYPE_NONE = 0; // 0x0
+    field @Deprecated public static final int URL_TYPE_PLUGIN = 5; // 0x5
+    field @Deprecated public static final int URL_TYPE_VIDEO = 2; // 0x2
+  }
+
+  @Deprecated @IntDef({androidx.browser.browseractions.BrowserActionsIntent.ITEM_INVALID_ITEM, androidx.browser.browseractions.BrowserActionsIntent.ITEM_OPEN_IN_NEW_TAB, androidx.browser.browseractions.BrowserActionsIntent.ITEM_OPEN_IN_INCOGNITO, androidx.browser.browseractions.BrowserActionsIntent.ITEM_DOWNLOAD, androidx.browser.browseractions.BrowserActionsIntent.ITEM_COPY, androidx.browser.browseractions.BrowserActionsIntent.ITEM_SHARE}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface BrowserActionsIntent.BrowserActionsItemId {
+  }
+
+  @Deprecated @IntDef({androidx.browser.browseractions.BrowserActionsIntent.URL_TYPE_NONE, androidx.browser.browseractions.BrowserActionsIntent.URL_TYPE_IMAGE, androidx.browser.browseractions.BrowserActionsIntent.URL_TYPE_VIDEO, androidx.browser.browseractions.BrowserActionsIntent.URL_TYPE_AUDIO, androidx.browser.browseractions.BrowserActionsIntent.URL_TYPE_FILE, androidx.browser.browseractions.BrowserActionsIntent.URL_TYPE_PLUGIN}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface BrowserActionsIntent.BrowserActionsUrlType {
+  }
+
+  @Deprecated public static final class BrowserActionsIntent.Builder {
+    ctor @Deprecated public BrowserActionsIntent.Builder(android.content.Context, android.net.Uri);
+    method @Deprecated public androidx.browser.browseractions.BrowserActionsIntent build();
+    method @Deprecated public androidx.browser.browseractions.BrowserActionsIntent.Builder setCustomItems(java.util.ArrayList<androidx.browser.browseractions.BrowserActionItem!>);
+    method @Deprecated public androidx.browser.browseractions.BrowserActionsIntent.Builder setCustomItems(androidx.browser.browseractions.BrowserActionItem!...);
+    method @Deprecated public androidx.browser.browseractions.BrowserActionsIntent.Builder setOnItemSelectedAction(android.app.PendingIntent);
+    method @Deprecated public androidx.browser.browseractions.BrowserActionsIntent.Builder setUrlType(@androidx.browser.browseractions.BrowserActionsIntent.BrowserActionsUrlType int);
+  }
+
+}
+
+package androidx.browser.customtabs {
+
+  public final class CustomTabColorSchemeParams {
+    field @ColorInt public final Integer? navigationBarColor;
+    field @ColorInt public final Integer? navigationBarDividerColor;
+    field @ColorInt public final Integer? secondaryToolbarColor;
+    field @ColorInt public final Integer? toolbarColor;
+  }
+
+  public static final class CustomTabColorSchemeParams.Builder {
+    ctor public CustomTabColorSchemeParams.Builder();
+    method public androidx.browser.customtabs.CustomTabColorSchemeParams build();
+    method public androidx.browser.customtabs.CustomTabColorSchemeParams.Builder setNavigationBarColor(@ColorInt int);
+    method public androidx.browser.customtabs.CustomTabColorSchemeParams.Builder setNavigationBarDividerColor(@ColorInt int);
+    method public androidx.browser.customtabs.CustomTabColorSchemeParams.Builder setSecondaryToolbarColor(@ColorInt int);
+    method public androidx.browser.customtabs.CustomTabColorSchemeParams.Builder setToolbarColor(@ColorInt int);
+  }
+
+  public class CustomTabsCallback {
+    ctor public CustomTabsCallback();
+    method public void extraCallback(String, android.os.Bundle?);
+    method public android.os.Bundle? extraCallbackWithResult(String, android.os.Bundle?);
+    method public void onActivityResized(@Dimension(unit=androidx.annotation.Dimension.PX) int, @Dimension(unit=androidx.annotation.Dimension.PX) int, android.os.Bundle);
+    method public void onMessageChannelReady(android.os.Bundle?);
+    method public void onNavigationEvent(int, android.os.Bundle?);
+    method public void onPostMessage(String, android.os.Bundle?);
+    method public void onRelationshipValidationResult(@androidx.browser.customtabs.CustomTabsService.Relation int, android.net.Uri, boolean, android.os.Bundle?);
+    field public static final int NAVIGATION_ABORTED = 4; // 0x4
+    field public static final int NAVIGATION_FAILED = 3; // 0x3
+    field public static final int NAVIGATION_FINISHED = 2; // 0x2
+    field public static final int NAVIGATION_STARTED = 1; // 0x1
+    field public static final int TAB_HIDDEN = 6; // 0x6
+    field public static final int TAB_SHOWN = 5; // 0x5
+  }
+
+  public class CustomTabsClient {
+    method public static boolean bindCustomTabsService(android.content.Context, String?, androidx.browser.customtabs.CustomTabsServiceConnection);
+    method public static boolean bindCustomTabsServicePreservePriority(android.content.Context, String?, androidx.browser.customtabs.CustomTabsServiceConnection);
+    method public static boolean connectAndInitialize(android.content.Context, String);
+    method public android.os.Bundle? extraCommand(String, android.os.Bundle?);
+    method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?);
+    method public static String? getPackageName(android.content.Context, java.util.List<java.lang.String!>?, boolean);
+    method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?);
+    method public androidx.browser.customtabs.CustomTabsSession? newSession(androidx.browser.customtabs.CustomTabsCallback?, int);
+    method public boolean warmup(long);
+  }
+
+  public final class CustomTabsIntent {
+    method public static int getActivityResizeBehavior(android.content.Intent);
+    method public static int getCloseButtonPosition(android.content.Intent);
+    method public static androidx.browser.customtabs.CustomTabColorSchemeParams getColorSchemeParams(android.content.Intent, int);
+    method @Dimension(unit=androidx.annotation.Dimension.PX) public static int getInitialActivityHeightPx(android.content.Intent);
+    method public static int getMaxToolbarItems();
+    method @Dimension(unit=androidx.annotation.Dimension.DP) public static int getToolbarCornerRadiusDp(android.content.Intent);
+    method public void launchUrl(android.content.Context, android.net.Uri);
+    method public static android.content.Intent setAlwaysUseBrowserUI(android.content.Intent?);
+    method public static boolean shouldAlwaysUseBrowserUI(android.content.Intent);
+    field public static final int ACTIVITY_HEIGHT_ADJUSTABLE = 1; // 0x1
+    field public static final int ACTIVITY_HEIGHT_DEFAULT = 0; // 0x0
+    field public static final int ACTIVITY_HEIGHT_FIXED = 2; // 0x2
+    field public static final int CLOSE_BUTTON_POSITION_DEFAULT = 0; // 0x0
+    field public static final int CLOSE_BUTTON_POSITION_END = 2; // 0x2
+    field public static final int CLOSE_BUTTON_POSITION_START = 1; // 0x1
+    field public static final int COLOR_SCHEME_DARK = 2; // 0x2
+    field public static final int COLOR_SCHEME_LIGHT = 1; // 0x1
+    field public static final int COLOR_SCHEME_SYSTEM = 0; // 0x0
+    field public static final String EXTRA_ACTION_BUTTON_BUNDLE = "android.support.customtabs.extra.ACTION_BUTTON_BUNDLE";
+    field public static final String EXTRA_ACTIVITY_HEIGHT_RESIZE_BEHAVIOR = "androidx.browser.customtabs.extra.ACTIVITY_HEIGHT_RESIZE_BEHAVIOR";
+    field public static final String EXTRA_CLOSE_BUTTON_ICON = "android.support.customtabs.extra.CLOSE_BUTTON_ICON";
+    field public static final String EXTRA_CLOSE_BUTTON_POSITION = "androidx.browser.customtabs.extra.CLOSE_BUTTON_POSITION";
+    field public static final String EXTRA_COLOR_SCHEME = "androidx.browser.customtabs.extra.COLOR_SCHEME";
+    field public static final String EXTRA_COLOR_SCHEME_PARAMS = "androidx.browser.customtabs.extra.COLOR_SCHEME_PARAMS";
+    field @Deprecated public static final String EXTRA_DEFAULT_SHARE_MENU_ITEM = "android.support.customtabs.extra.SHARE_MENU_ITEM";
+    field public static final String EXTRA_ENABLE_INSTANT_APPS = "android.support.customtabs.extra.EXTRA_ENABLE_INSTANT_APPS";
+    field public static final String EXTRA_ENABLE_URLBAR_HIDING = "android.support.customtabs.extra.ENABLE_URLBAR_HIDING";
+    field public static final String EXTRA_EXIT_ANIMATION_BUNDLE = "android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE";
+    field public static final String EXTRA_INITIAL_ACTIVITY_HEIGHT_PX = "androidx.browser.customtabs.extra.INITIAL_ACTIVITY_HEIGHT_PX";
+    field public static final String EXTRA_MENU_ITEMS = "android.support.customtabs.extra.MENU_ITEMS";
+    field public static final String EXTRA_NAVIGATION_BAR_COLOR = "androidx.browser.customtabs.extra.NAVIGATION_BAR_COLOR";
+    field public static final String EXTRA_NAVIGATION_BAR_DIVIDER_COLOR = "androidx.browser.customtabs.extra.NAVIGATION_BAR_DIVIDER_COLOR";
+    field public static final String EXTRA_REMOTEVIEWS = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS";
+    field public static final String EXTRA_REMOTEVIEWS_CLICKED_ID = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_CLICKED_ID";
+    field public static final String EXTRA_REMOTEVIEWS_PENDINGINTENT = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_PENDINGINTENT";
+    field public static final String EXTRA_REMOTEVIEWS_VIEW_IDS = "android.support.customtabs.extra.EXTRA_REMOTEVIEWS_VIEW_IDS";
+    field public static final String EXTRA_SECONDARY_TOOLBAR_COLOR = "android.support.customtabs.extra.SECONDARY_TOOLBAR_COLOR";
+    field public static final String EXTRA_SESSION = "android.support.customtabs.extra.SESSION";
+    field public static final String EXTRA_SHARE_STATE = "androidx.browser.customtabs.extra.SHARE_STATE";
+    field public static final String EXTRA_TINT_ACTION_BUTTON = "android.support.customtabs.extra.TINT_ACTION_BUTTON";
+    field public static final String EXTRA_TITLE_VISIBILITY_STATE = "android.support.customtabs.extra.TITLE_VISIBILITY";
+    field public static final String EXTRA_TOOLBAR_COLOR = "android.support.customtabs.extra.TOOLBAR_COLOR";
+    field public static final String EXTRA_TOOLBAR_CORNER_RADIUS_DP = "androidx.browser.customtabs.extra.TOOLBAR_CORNER_RADIUS_DP";
+    field public static final String EXTRA_TOOLBAR_ITEMS = "android.support.customtabs.extra.TOOLBAR_ITEMS";
+    field public static final String KEY_DESCRIPTION = "android.support.customtabs.customaction.DESCRIPTION";
+    field public static final String KEY_ICON = "android.support.customtabs.customaction.ICON";
+    field public static final String KEY_ID = "android.support.customtabs.customaction.ID";
+    field public static final String KEY_MENU_ITEM_TITLE = "android.support.customtabs.customaction.MENU_ITEM_TITLE";
+    field public static final String KEY_PENDING_INTENT = "android.support.customtabs.customaction.PENDING_INTENT";
+    field public static final int NO_TITLE = 0; // 0x0
+    field public static final int SHARE_STATE_DEFAULT = 0; // 0x0
+    field public static final int SHARE_STATE_OFF = 2; // 0x2
+    field public static final int SHARE_STATE_ON = 1; // 0x1
+    field public static final int SHOW_PAGE_TITLE = 1; // 0x1
+    field public static final int TOOLBAR_ACTION_BUTTON_ID = 0; // 0x0
+    field public final android.content.Intent intent;
+    field public final android.os.Bundle? startAnimationBundle;
+  }
+
+  public static final class CustomTabsIntent.Builder {
+    ctor public CustomTabsIntent.Builder();
+    ctor public CustomTabsIntent.Builder(androidx.browser.customtabs.CustomTabsSession?);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder addDefaultShareMenuItem();
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder addMenuItem(String, android.app.PendingIntent);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder addToolbarItem(int, android.graphics.Bitmap, String, android.app.PendingIntent) throws java.lang.IllegalStateException;
+    method public androidx.browser.customtabs.CustomTabsIntent build();
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder enableUrlBarHiding();
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setActionButton(android.graphics.Bitmap, String, android.app.PendingIntent, boolean);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setActionButton(android.graphics.Bitmap, String, android.app.PendingIntent);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setCloseButtonIcon(android.graphics.Bitmap);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setCloseButtonPosition(int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setColorScheme(int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setColorSchemeParams(int, androidx.browser.customtabs.CustomTabColorSchemeParams);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setDefaultColorSchemeParams(androidx.browser.customtabs.CustomTabColorSchemeParams);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setDefaultShareMenuItemEnabled(boolean);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setExitAnimations(android.content.Context, @AnimRes int, @AnimRes int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setInitialActivityHeightPx(@Dimension(unit=androidx.annotation.Dimension.PX) int, int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setInitialActivityHeightPx(@Dimension(unit=androidx.annotation.Dimension.PX) int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setInstantAppsEnabled(boolean);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setNavigationBarColor(@ColorInt int);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setNavigationBarDividerColor(@ColorInt int);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarColor(@ColorInt int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setSecondaryToolbarViews(android.widget.RemoteViews, int[]?, android.app.PendingIntent?);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setSession(androidx.browser.customtabs.CustomTabsSession);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setShareState(int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setShowTitle(boolean);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setStartAnimations(android.content.Context, @AnimRes int, @AnimRes int);
+    method @Deprecated public androidx.browser.customtabs.CustomTabsIntent.Builder setToolbarColor(@ColorInt int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setToolbarCornerRadiusDp(@Dimension(unit=androidx.annotation.Dimension.DP) int);
+    method public androidx.browser.customtabs.CustomTabsIntent.Builder setUrlBarHidingEnabled(boolean);
+  }
+
+  public abstract class CustomTabsService extends android.app.Service {
+    ctor public CustomTabsService();
+    method protected boolean cleanUpSession(androidx.browser.customtabs.CustomTabsSessionToken);
+    method protected abstract android.os.Bundle? extraCommand(String, android.os.Bundle?);
+    method protected abstract boolean mayLaunchUrl(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri?, android.os.Bundle?, java.util.List<android.os.Bundle!>?);
+    method protected abstract boolean newSession(androidx.browser.customtabs.CustomTabsSessionToken);
+    method public android.os.IBinder onBind(android.content.Intent?);
+    method @androidx.browser.customtabs.CustomTabsService.Result protected abstract int postMessage(androidx.browser.customtabs.CustomTabsSessionToken, String, android.os.Bundle?);
+    method protected abstract boolean receiveFile(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri, int, android.os.Bundle?);
+    method protected abstract boolean requestPostMessageChannel(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri);
+    method protected abstract boolean updateVisuals(androidx.browser.customtabs.CustomTabsSessionToken, android.os.Bundle?);
+    method protected abstract boolean validateRelationship(androidx.browser.customtabs.CustomTabsSessionToken, @androidx.browser.customtabs.CustomTabsService.Relation int, android.net.Uri, android.os.Bundle?);
+    method protected abstract boolean warmup(long);
+    field public static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService";
+    field public static final String CATEGORY_COLOR_SCHEME_CUSTOMIZATION = "androidx.browser.customtabs.category.ColorSchemeCustomization";
+    field public static final String CATEGORY_NAVBAR_COLOR_CUSTOMIZATION = "androidx.browser.customtabs.category.NavBarColorCustomization";
+    field public static final String CATEGORY_TRUSTED_WEB_ACTIVITY_IMMERSIVE_MODE = "androidx.browser.trusted.category.ImmersiveMode";
+    field public static final String CATEGORY_WEB_SHARE_TARGET_V2 = "androidx.browser.trusted.category.WebShareTargetV2";
+    field public static final int FILE_PURPOSE_TRUSTED_WEB_ACTIVITY_SPLASH_IMAGE = 1; // 0x1
+    field public static final String KEY_SUCCESS = "androidx.browser.customtabs.SUCCESS";
+    field public static final String KEY_URL = "android.support.customtabs.otherurls.URL";
+    field public static final int RELATION_HANDLE_ALL_URLS = 2; // 0x2
+    field public static final int RELATION_USE_AS_ORIGIN = 1; // 0x1
+    field public static final int RESULT_FAILURE_DISALLOWED = -1; // 0xffffffff
+    field public static final int RESULT_FAILURE_MESSAGING_ERROR = -3; // 0xfffffffd
+    field public static final int RESULT_FAILURE_REMOTE_ERROR = -2; // 0xfffffffe
+    field public static final int RESULT_SUCCESS = 0; // 0x0
+    field public static final String TRUSTED_WEB_ACTIVITY_CATEGORY = "androidx.browser.trusted.category.TrustedWebActivities";
+  }
+
+  @IntDef({androidx.browser.customtabs.CustomTabsService.RELATION_USE_AS_ORIGIN, androidx.browser.customtabs.CustomTabsService.RELATION_HANDLE_ALL_URLS}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface CustomTabsService.Relation {
+  }
+
+  @IntDef({androidx.browser.customtabs.CustomTabsService.RESULT_SUCCESS, androidx.browser.customtabs.CustomTabsService.RESULT_FAILURE_DISALLOWED, androidx.browser.customtabs.CustomTabsService.RESULT_FAILURE_REMOTE_ERROR, androidx.browser.customtabs.CustomTabsService.RESULT_FAILURE_MESSAGING_ERROR}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface CustomTabsService.Result {
+  }
+
+  public abstract class CustomTabsServiceConnection implements android.content.ServiceConnection {
+    ctor public CustomTabsServiceConnection();
+    method public abstract void onCustomTabsServiceConnected(android.content.ComponentName, androidx.browser.customtabs.CustomTabsClient);
+    method public final void onServiceConnected(android.content.ComponentName, android.os.IBinder);
+  }
+
+  public final class CustomTabsSession {
+    method @VisibleForTesting public static androidx.browser.customtabs.CustomTabsSession createMockSessionForTesting(android.content.ComponentName);
+    method public boolean mayLaunchUrl(android.net.Uri?, android.os.Bundle?, java.util.List<android.os.Bundle!>?);
+    method @androidx.browser.customtabs.CustomTabsService.Result public int postMessage(String, android.os.Bundle?);
+    method public boolean receiveFile(android.net.Uri, int, android.os.Bundle?);
+    method public boolean requestPostMessageChannel(android.net.Uri);
+    method public boolean setActionButton(android.graphics.Bitmap, String);
+    method public boolean setSecondaryToolbarViews(android.widget.RemoteViews?, int[]?, android.app.PendingIntent?);
+    method @Deprecated public boolean setToolbarItem(int, android.graphics.Bitmap, String);
+    method public boolean validateRelationship(@androidx.browser.customtabs.CustomTabsService.Relation int, android.net.Uri, android.os.Bundle?);
+  }
+
+  public class CustomTabsSessionToken {
+    method public static androidx.browser.customtabs.CustomTabsSessionToken createMockSessionTokenForTesting();
+    method public androidx.browser.customtabs.CustomTabsCallback? getCallback();
+    method public static androidx.browser.customtabs.CustomTabsSessionToken? getSessionTokenFromIntent(android.content.Intent);
+    method public boolean isAssociatedWith(androidx.browser.customtabs.CustomTabsSession);
+  }
+
+  public class PostMessageService extends android.app.Service {
+    ctor public PostMessageService();
+    method public android.os.IBinder onBind(android.content.Intent?);
+  }
+
+  public abstract class PostMessageServiceConnection implements android.content.ServiceConnection {
+    ctor public PostMessageServiceConnection(androidx.browser.customtabs.CustomTabsSessionToken);
+    method public boolean bindSessionToPostMessageService(android.content.Context, String);
+    method public final boolean notifyMessageChannelReady(android.os.Bundle?);
+    method public void onPostMessageServiceConnected();
+    method public void onPostMessageServiceDisconnected();
+    method public final void onServiceConnected(android.content.ComponentName, android.os.IBinder);
+    method public final void onServiceDisconnected(android.content.ComponentName);
+    method public final boolean postMessage(String, android.os.Bundle?);
+    method public void unbindFromContext(android.content.Context);
+  }
+
+  public class TrustedWebUtils {
+    method public static boolean areSplashScreensSupported(android.content.Context, String, String);
+    method @Deprecated public static void launchAsTrustedWebActivity(android.content.Context, androidx.browser.customtabs.CustomTabsIntent, android.net.Uri);
+    method @WorkerThread public static boolean transferSplashImage(android.content.Context, java.io.File, String, String, androidx.browser.customtabs.CustomTabsSession);
+    field public static final String EXTRA_LAUNCH_AS_TRUSTED_WEB_ACTIVITY = "android.support.customtabs.extra.LAUNCH_AS_TRUSTED_WEB_ACTIVITY";
+  }
+
+}
+
+package androidx.browser.trusted {
+
+  public final class ScreenOrientation {
+    field public static final int ANY = 5; // 0x5
+    field public static final int DEFAULT = 0; // 0x0
+    field public static final int LANDSCAPE = 6; // 0x6
+    field public static final int LANDSCAPE_PRIMARY = 3; // 0x3
+    field public static final int LANDSCAPE_SECONDARY = 4; // 0x4
+    field public static final int NATURAL = 8; // 0x8
+    field public static final int PORTRAIT = 7; // 0x7
+    field public static final int PORTRAIT_PRIMARY = 1; // 0x1
+    field public static final int PORTRAIT_SECONDARY = 2; // 0x2
+  }
+
+  public final class Token {
+    method public static androidx.browser.trusted.Token? create(String, android.content.pm.PackageManager);
+    method public static androidx.browser.trusted.Token deserialize(byte[]);
+    method public boolean matches(String, android.content.pm.PackageManager);
+    method public byte[] serialize();
+  }
+
+  public interface TokenStore {
+    method @BinderThread public androidx.browser.trusted.Token? load();
+    method @WorkerThread public void store(androidx.browser.trusted.Token?);
+  }
+
+  public abstract class TrustedWebActivityCallback {
+    ctor public TrustedWebActivityCallback();
+    method public abstract void onExtraCallback(String, android.os.Bundle?);
+  }
+
+  public class TrustedWebActivityCallbackRemote {
+    method public void runExtraCallback(String, android.os.Bundle) throws android.os.RemoteException;
+  }
+
+  public interface TrustedWebActivityDisplayMode {
+    method public static androidx.browser.trusted.TrustedWebActivityDisplayMode fromBundle(android.os.Bundle);
+    method public android.os.Bundle toBundle();
+    field public static final String KEY_ID = "androidx.browser.trusted.displaymode.KEY_ID";
+  }
+
+  public static class TrustedWebActivityDisplayMode.DefaultMode implements androidx.browser.trusted.TrustedWebActivityDisplayMode {
+    ctor public TrustedWebActivityDisplayMode.DefaultMode();
+    method public android.os.Bundle toBundle();
+  }
+
+  public static class TrustedWebActivityDisplayMode.ImmersiveMode implements androidx.browser.trusted.TrustedWebActivityDisplayMode {
+    ctor public TrustedWebActivityDisplayMode.ImmersiveMode(boolean, int);
+    method public boolean isSticky();
+    method public int layoutInDisplayCutoutMode();
+    method public android.os.Bundle toBundle();
+    field public static final String KEY_CUTOUT_MODE = "androidx.browser.trusted.displaymode.KEY_CUTOUT_MODE";
+    field public static final String KEY_STICKY = "androidx.browser.trusted.displaymode.KEY_STICKY";
+  }
+
+  public final class TrustedWebActivityIntent {
+    method public android.content.Intent getIntent();
+    method public void launchTrustedWebActivity(android.content.Context);
+  }
+
+  public class TrustedWebActivityIntentBuilder {
+    ctor public TrustedWebActivityIntentBuilder(android.net.Uri);
+    method public androidx.browser.trusted.TrustedWebActivityIntent build(androidx.browser.customtabs.CustomTabsSession);
+    method public androidx.browser.customtabs.CustomTabsIntent buildCustomTabsIntent();
+    method public androidx.browser.trusted.TrustedWebActivityDisplayMode getDisplayMode();
+    method public android.net.Uri getUri();
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setAdditionalTrustedOrigins(java.util.List<java.lang.String!>);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setColorScheme(int);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setColorSchemeParams(int, androidx.browser.customtabs.CustomTabColorSchemeParams);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setDefaultColorSchemeParams(androidx.browser.customtabs.CustomTabColorSchemeParams);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setDisplayMode(androidx.browser.trusted.TrustedWebActivityDisplayMode);
+    method @Deprecated public androidx.browser.trusted.TrustedWebActivityIntentBuilder setNavigationBarColor(@ColorInt int);
+    method @Deprecated public androidx.browser.trusted.TrustedWebActivityIntentBuilder setNavigationBarDividerColor(@ColorInt int);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setScreenOrientation(int);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setShareParams(androidx.browser.trusted.sharing.ShareTarget, androidx.browser.trusted.sharing.ShareData);
+    method public androidx.browser.trusted.TrustedWebActivityIntentBuilder setSplashScreenParams(android.os.Bundle);
+    method @Deprecated public androidx.browser.trusted.TrustedWebActivityIntentBuilder setToolbarColor(@ColorInt int);
+    field public static final String EXTRA_ADDITIONAL_TRUSTED_ORIGINS = "android.support.customtabs.extra.ADDITIONAL_TRUSTED_ORIGINS";
+    field public static final String EXTRA_DISPLAY_MODE = "androidx.browser.trusted.extra.DISPLAY_MODE";
+    field public static final String EXTRA_SCREEN_ORIENTATION = "androidx.browser.trusted.extra.SCREEN_ORIENTATION";
+    field public static final String EXTRA_SHARE_DATA = "androidx.browser.trusted.extra.SHARE_DATA";
+    field public static final String EXTRA_SHARE_TARGET = "androidx.browser.trusted.extra.SHARE_TARGET";
+    field public static final String EXTRA_SPLASH_SCREEN_PARAMS = "androidx.browser.trusted.EXTRA_SPLASH_SCREEN_PARAMS";
+  }
+
+  public abstract class TrustedWebActivityService extends android.app.Service {
+    ctor public TrustedWebActivityService();
+    method @BinderThread public abstract androidx.browser.trusted.TokenStore getTokenStore();
+    method @BinderThread public boolean onAreNotificationsEnabled(String);
+    method @MainThread public final android.os.IBinder? onBind(android.content.Intent?);
+    method @BinderThread public void onCancelNotification(String, int);
+    method @BinderThread public android.os.Bundle? onExtraCommand(String, android.os.Bundle, androidx.browser.trusted.TrustedWebActivityCallbackRemote?);
+    method @BinderThread public android.os.Bundle onGetSmallIconBitmap();
+    method @BinderThread public int onGetSmallIconId();
+    method @BinderThread @RequiresPermission(android.Manifest.permission.POST_NOTIFICATIONS) public boolean onNotifyNotificationWithChannel(String, int, android.app.Notification, String);
+    method @MainThread public final boolean onUnbind(android.content.Intent?);
+    field public static final String ACTION_TRUSTED_WEB_ACTIVITY_SERVICE = "android.support.customtabs.trusted.TRUSTED_WEB_ACTIVITY_SERVICE";
+    field public static final String KEY_SMALL_ICON_BITMAP = "android.support.customtabs.trusted.SMALL_ICON_BITMAP";
+    field public static final String KEY_SUCCESS = "androidx.browser.trusted.SUCCESS";
+    field public static final String META_DATA_NAME_SMALL_ICON = "android.support.customtabs.trusted.SMALL_ICON";
+    field public static final int SMALL_ICON_NOT_SET = -1; // 0xffffffff
+  }
+
+  public final class TrustedWebActivityServiceConnection {
+    method public boolean areNotificationsEnabled(String) throws android.os.RemoteException;
+    method public void cancel(String, int) throws android.os.RemoteException;
+    method public android.content.ComponentName getComponentName();
+    method public android.graphics.Bitmap? getSmallIconBitmap() throws android.os.RemoteException;
+    method public int getSmallIconId() throws android.os.RemoteException;
+    method public boolean notify(String, int, android.app.Notification, String) throws android.os.RemoteException;
+    method public android.os.Bundle? sendExtraCommand(String, android.os.Bundle, androidx.browser.trusted.TrustedWebActivityCallback?) throws android.os.RemoteException;
+  }
+
+  public final class TrustedWebActivityServiceConnectionPool {
+    method @MainThread public com.google.common.util.concurrent.ListenableFuture<androidx.browser.trusted.TrustedWebActivityServiceConnection!> connect(android.net.Uri, java.util.Set<androidx.browser.trusted.Token!>, java.util.concurrent.Executor);
+    method public static androidx.browser.trusted.TrustedWebActivityServiceConnectionPool create(android.content.Context);
+    method @MainThread public boolean serviceExistsForScope(android.net.Uri, java.util.Set<androidx.browser.trusted.Token!>);
+  }
+
+}
+
+package androidx.browser.trusted.sharing {
+
+  public final class ShareData {
+    ctor public ShareData(String?, String?, java.util.List<android.net.Uri!>?);
+    method public static androidx.browser.trusted.sharing.ShareData fromBundle(android.os.Bundle);
+    method public android.os.Bundle toBundle();
+    field public static final String KEY_TEXT = "androidx.browser.trusted.sharing.KEY_TEXT";
+    field public static final String KEY_TITLE = "androidx.browser.trusted.sharing.KEY_TITLE";
+    field public static final String KEY_URIS = "androidx.browser.trusted.sharing.KEY_URIS";
+    field public final String? text;
+    field public final String? title;
+    field public final java.util.List<android.net.Uri!>? uris;
+  }
+
+  public final class ShareTarget {
+    ctor public ShareTarget(String, String?, String?, androidx.browser.trusted.sharing.ShareTarget.Params);
+    method public static androidx.browser.trusted.sharing.ShareTarget? fromBundle(android.os.Bundle);
+    method public android.os.Bundle toBundle();
+    field public static final String ENCODING_TYPE_MULTIPART = "multipart/form-data";
+    field public static final String ENCODING_TYPE_URL_ENCODED = "application/x-www-form-urlencoded";
+    field public static final String KEY_ACTION = "androidx.browser.trusted.sharing.KEY_ACTION";
+    field public static final String KEY_ENCTYPE = "androidx.browser.trusted.sharing.KEY_ENCTYPE";
+    field public static final String KEY_METHOD = "androidx.browser.trusted.sharing.KEY_METHOD";
+    field public static final String KEY_PARAMS = "androidx.browser.trusted.sharing.KEY_PARAMS";
+    field public static final String METHOD_GET = "GET";
+    field public static final String METHOD_POST = "POST";
+    field public final String action;
+    field public final String? encodingType;
+    field public final String? method;
+    field public final androidx.browser.trusted.sharing.ShareTarget.Params params;
+  }
+
+  public static final class ShareTarget.FileFormField {
+    ctor public ShareTarget.FileFormField(String, java.util.List<java.lang.String!>);
+    field public static final String KEY_ACCEPTED_TYPES = "androidx.browser.trusted.sharing.KEY_ACCEPTED_TYPES";
+    field public static final String KEY_NAME = "androidx.browser.trusted.sharing.KEY_FILE_NAME";
+    field public final java.util.List<java.lang.String!> acceptedTypes;
+    field public final String name;
+  }
+
+  public static class ShareTarget.Params {
+    ctor public ShareTarget.Params(String?, String?, java.util.List<androidx.browser.trusted.sharing.ShareTarget.FileFormField!>?);
+    field public static final String KEY_FILES = "androidx.browser.trusted.sharing.KEY_FILES";
+    field public static final String KEY_TEXT = "androidx.browser.trusted.sharing.KEY_TEXT";
+    field public static final String KEY_TITLE = "androidx.browser.trusted.sharing.KEY_TITLE";
+    field public final java.util.List<androidx.browser.trusted.sharing.ShareTarget.FileFormField!>? files;
+    field public final String? text;
+    field public final String? title;
+  }
+
+}
+
+package androidx.browser.trusted.splashscreens {
+
+  public final class SplashScreenParamKey {
+    field public static final String KEY_BACKGROUND_COLOR = "androidx.browser.trusted.trusted.KEY_SPLASH_SCREEN_BACKGROUND_COLOR";
+    field public static final String KEY_FADE_OUT_DURATION_MS = "androidx.browser.trusted.KEY_SPLASH_SCREEN_FADE_OUT_DURATION";
+    field public static final String KEY_IMAGE_TRANSFORMATION_MATRIX = "androidx.browser.trusted.KEY_SPLASH_SCREEN_TRANSFORMATION_MATRIX";
+    field public static final String KEY_SCALE_TYPE = "androidx.browser.trusted.KEY_SPLASH_SCREEN_SCALE_TYPE";
+    field public static final String KEY_VERSION = "androidx.browser.trusted.KEY_SPLASH_SCREEN_VERSION";
+  }
+
+  public final class SplashScreenVersion {
+    field public static final String V1 = "androidx.browser.trusted.category.TrustedWebActivitySplashScreensV1";
+  }
+
+}
+
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/MavenUploadHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
index 9a796c7e..b7664e3 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
@@ -16,6 +16,7 @@
 
 package androidx.build
 
+import com.android.build.gradle.AppPlugin
 import com.android.build.gradle.LibraryPlugin
 import com.google.gson.GsonBuilder
 import com.google.gson.JsonObject
@@ -81,6 +82,21 @@
             }
         }
     }
+    // validate that all libraries that should be published actually get registered.
+    gradle.taskGraph.whenReady {
+        if (releaseTaskShouldBeRegistered(extension)) {
+            tasks.findByName(Release.PROJECT_ARCHIVE_ZIP_TASK_NAME)
+                ?: throw GradleException("Project $name is configured for publishing, but a " +
+                    "'createProjectZip' task was never registered. This is likely a bug in" +
+                    "AndroidX plugin configuration")
+        }
+    }
+}
+
+private fun Project.releaseTaskShouldBeRegistered(extension: AndroidXExtension): Boolean {
+    if (plugins.hasPlugin(AppPlugin::class.java)) { return false }
+    if (!extension.shouldRelease() && !isSnapshotBuild()) { return false }
+    return extension.shouldPublish()
 }
 
 /**
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index 89d20ff..e126ddc 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -59,6 +59,7 @@
 import org.gradle.api.provider.ListProperty
 import org.gradle.api.provider.Provider
 import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Input
 import org.gradle.api.tasks.OutputDirectory
 import org.gradle.api.tasks.InputFiles
 import org.gradle.api.tasks.OutputFile
@@ -662,7 +663,7 @@
 @CacheableTask
 abstract class UnzipMultiplatformSourcesTask() : DefaultTask() {
 
-    @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE)
+    @get:Input
     abstract val inputJars: ListProperty<File>
 
     @OutputDirectory
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-studio-integration-vitals.sh b/busytown/androidx-studio-integration-vitals.sh
index f96ca2f..4462a891 100755
--- a/busytown/androidx-studio-integration-vitals.sh
+++ b/busytown/androidx-studio-integration-vitals.sh
@@ -1,9 +1,9 @@
 set -e
 SCRIPT_PATH="$(cd $(dirname $0) && pwd)"
 
-"$SCRIPT_PATH"/impl/build-studio-and-androidx.sh \
-    -Pandroidx.summarizeStderr \
-    tasks \
-    :navigation:navigation-safe-args-gradle-plugin:test \
-    --stacktrace \
-    --no-daemon
+#"$SCRIPT_PATH"/impl/build-studio-and-androidx.sh \
+#    -Pandroidx.summarizeStderr \
+#    tasks \
+#    :navigation:navigation-safe-args-gradle-plugin:test \
+#    --stacktrace \
+#    --no-daemon
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 04b67f6..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
@@ -18,11 +18,17 @@
 
 import android.content.Context
 import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraCharacteristics.CONTROL_MAX_REGIONS_AE
+import android.hardware.camera2.CameraCharacteristics.CONTROL_MAX_REGIONS_AF
+import android.hardware.camera2.CameraCharacteristics.CONTROL_MAX_REGIONS_AWB
 import android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON
 import android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH
 import android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH
 import android.hardware.camera2.CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION
 import android.hardware.camera2.CaptureRequest.CONTROL_AE_MODE
+import android.hardware.camera2.CaptureRequest.CONTROL_AE_REGIONS
+import android.hardware.camera2.CaptureRequest.CONTROL_AF_REGIONS
+import android.hardware.camera2.CaptureRequest.CONTROL_AWB_REGIONS
 import android.hardware.camera2.CaptureRequest.CONTROL_CAPTURE_INTENT
 import android.hardware.camera2.CaptureRequest.CONTROL_CAPTURE_INTENT_CUSTOM
 import android.hardware.camera2.CaptureRequest.CONTROL_ZOOM_RATIO
@@ -30,6 +36,7 @@
 import android.hardware.camera2.CaptureRequest.FLASH_MODE_TORCH
 import android.hardware.camera2.CaptureRequest.SCALER_CROP_REGION
 import android.os.Build
+import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.FrameInfo
 import androidx.camera.camera2.pipe.RequestMetadata
 import androidx.camera.camera2.pipe.integration.adapter.CameraControlAdapter
@@ -37,20 +44,25 @@
 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
 import androidx.camera.core.ImageCapture
+import androidx.camera.core.SurfaceOrientedMeteringPointFactory
 import androidx.camera.core.UseCase
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
-import androidx.camera.testing.LabTestRule
 import androidx.concurrent.futures.await
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 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
 import kotlinx.coroutines.runBlocking
@@ -58,13 +70,13 @@
 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
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.concurrent.TimeUnit
 
 private val TIMEOUT = TimeUnit.SECONDS.toMillis(10)
 
@@ -92,10 +104,6 @@
     @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest()
 
-    // TODO(b/187015621): Remove the rule after the surface can be safely closed.
-    @get:Rule
-    val labTest: LabTestRule = LabTestRule()
-
     @Before
     fun setUp() {
         Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
@@ -122,9 +130,11 @@
     }
 
     @After
-    fun tearDown() {
+    fun tearDown(): Unit = runBlocking {
         if (::camera.isInitialized) {
-            camera.detachUseCases()
+            withContext(Dispatchers.Main) {
+                camera.detachUseCases()
+            }
         }
 
         CameraXUtil.shutdown()[10000, TimeUnit.MILLISECONDS]
@@ -132,7 +142,6 @@
 
     // TODO: test all public API of the CameraControl to ensure the RequestOptions still exist
     //  after adding/removing the UseCase.
-    @LabTestRule.LabTestOnly
     @Test
     fun addUseCase_requestOptionsShouldSetToCamera(): Unit = runBlocking {
         // Arrange.
@@ -153,7 +162,6 @@
 
     // TODO: test all public API of the CameraControl to ensure the RequestOptions still exist
     //  after adding/removing the UseCase.
-    @LabTestRule.LabTestOnly
     @Test
     fun removeUseCase_requestOptionsShouldSetToCamera(): Unit = runBlocking {
         // Arrange.
@@ -172,7 +180,6 @@
         verifyRequestOptions()
     }
 
-    @LabTestRule.LabTestOnly
     @Test
     fun setFlashModeAuto_aeModeSetAndRequestUpdated(): Unit = runBlocking {
         Assume.assumeTrue(hasFlashUnit)
@@ -188,7 +195,6 @@
         Truth.assertThat(cameraControl.flashMode).isEqualTo(ImageCapture.FLASH_MODE_AUTO)
     }
 
-    @LabTestRule.LabTestOnly
     @Test
     fun setFlashModeOff_aeModeSetAndRequestUpdated(): Unit = runBlocking {
         Assume.assumeTrue(hasFlashUnit)
@@ -204,7 +210,6 @@
         Truth.assertThat(cameraControl.flashMode).isEqualTo(ImageCapture.FLASH_MODE_OFF)
     }
 
-    @LabTestRule.LabTestOnly
     @Test
     fun setFlashModeOn_aeModeSetAndRequestUpdated(): Unit = runBlocking {
         Assume.assumeTrue(hasFlashUnit)
@@ -220,7 +225,6 @@
         Truth.assertThat(cameraControl.flashMode).isEqualTo(ImageCapture.FLASH_MODE_ON)
     }
 
-    @LabTestRule.LabTestOnly
     @Test
     fun enableTorch_aeModeSetAndRequestUpdated(): Unit = runBlocking {
         Assume.assumeTrue(hasFlashUnit)
@@ -236,7 +240,6 @@
         )
     }
 
-    @LabTestRule.LabTestOnly
     @Test
     fun disableTorchFlashModeAuto_aeModeSetAndRequestUpdated(): Unit = runBlocking {
         Assume.assumeTrue(hasFlashUnit)
@@ -253,8 +256,109 @@
         )
     }
 
+    @Test
+    fun startFocusAndMetering_3ARegionsUpdated() = runBlocking {
+        Assume.assumeTrue(
+            characteristics.getMaxRegionCount(CONTROL_MAX_REGIONS_AF) > 0 ||
+                characteristics.getMaxRegionCount(CONTROL_MAX_REGIONS_AE) > 0 ||
+                characteristics.getMaxRegionCount(CONTROL_MAX_REGIONS_AWB) > 0
+        )
+        val factory = SurfaceOrientedMeteringPointFactory(1.0f, 1.0f)
+        val action = FocusMeteringAction.Builder(factory.createPoint(0f, 0f)).build()
+        bindUseCase(imageAnalysis)
+
+        // Act.
+        cameraControl.startFocusAndMetering(action).await()
+
+        // Assert. Here we verify only 3A region count is correct.
+        val expectedAfCount =
+            characteristics.getMaxRegionCount(CONTROL_MAX_REGIONS_AF).coerceAtMost(1)
+        val expectedAeCount =
+            characteristics.getMaxRegionCount(CONTROL_MAX_REGIONS_AE).coerceAtMost(1)
+        val expectedAwbCount =
+            characteristics.getMaxRegionCount(CONTROL_MAX_REGIONS_AWB).coerceAtMost(1)
+        waitForResult(captureCount = 60).verify(
+            { requestMeta: RequestMetadata, _ ->
+                val afRegionMatched = requestMeta.getOrDefault(
+                    CONTROL_AF_REGIONS,
+                    emptyArray()
+                ).size == expectedAfCount
+
+                val aeRegionMatched = requestMeta.getOrDefault(
+                    CONTROL_AE_REGIONS,
+                    emptyArray()
+                ).size == expectedAeCount
+
+                val awbRegionMatched = requestMeta.getOrDefault(
+                    CONTROL_AWB_REGIONS,
+                    emptyArray()
+                ).size == expectedAwbCount
+
+                afRegionMatched && aeRegionMatched && awbRegionMatched
+            },
+            TIMEOUT
+        )
+    }
+
+    @Test
+    fun cancelFocusAndMetering_3ARegionsReset() = runBlocking {
+        Assume.assumeTrue(
+            characteristics.getMaxRegionCount(CONTROL_MAX_REGIONS_AF) > 0 ||
+                characteristics.getMaxRegionCount(CONTROL_MAX_REGIONS_AE) > 0 ||
+                characteristics.getMaxRegionCount(CONTROL_MAX_REGIONS_AWB) > 0
+        )
+        val factory = SurfaceOrientedMeteringPointFactory(1.0f, 1.0f)
+        val action = FocusMeteringAction.Builder(factory.createPoint(0f, 0f)).build()
+        bindUseCase(imageAnalysis)
+
+        // Act.
+        cameraControl.startFocusAndMetering(action).await()
+        cameraControl.cancelFocusAndMetering().await()
+
+        // Assert. The regions are reset to the default.
+        waitForResult(captureCount = 60).verify(
+            { requestMeta: RequestMetadata, _ ->
+
+                val isDefaultAfRegion = requestMeta.getOrDefault(
+                    CONTROL_AF_REGIONS,
+                    CameraGraph.Constants3A.METERING_REGIONS_DEFAULT
+                ).contentEquals(CameraGraph.Constants3A.METERING_REGIONS_DEFAULT)
+
+                val isDefaultAeRegion = requestMeta.getOrDefault(
+                    CONTROL_AE_REGIONS,
+                    CameraGraph.Constants3A.METERING_REGIONS_DEFAULT
+                ).contentEquals(CameraGraph.Constants3A.METERING_REGIONS_DEFAULT)
+
+                val isDefaultAwbRegion = requestMeta.getOrDefault(
+                    CONTROL_AWB_REGIONS,
+                    CameraGraph.Constants3A.METERING_REGIONS_DEFAULT
+                ).contentEquals(CameraGraph.Constants3A.METERING_REGIONS_DEFAULT)
+
+                isDefaultAfRegion && isDefaultAeRegion && isDefaultAwbRegion
+            },
+            TIMEOUT
+        )
+    }
+
+    @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(
@@ -262,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(
@@ -331,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/integration/CaptureConfigAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
index 7f95975..3f62b210 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
@@ -36,7 +36,6 @@
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
-import androidx.camera.testing.LabTestRule
 import androidx.camera.testing.fakes.FakeUseCase
 import androidx.camera.testing.fakes.FakeUseCaseConfig
 import androidx.test.core.app.ApplicationProvider
@@ -70,10 +69,6 @@
     @get:Rule
     val useCamera: TestRule = CameraUtil.grantCameraPermissionAndPreTest()
 
-    // TODO(b/187015621): Remove the rule after the surface can be safely closed.
-    @get:Rule
-    val labTest: LabTestRule = LabTestRule()
-
     private var cameraControl: CameraControlAdapter? = null
     private var camera: CameraUseCaseAdapter? = null
     private lateinit var testDeferrableSurface: TestDeferrableSurface
@@ -119,7 +114,6 @@
     }
 
     @Test
-    @LabTestRule.LabTestOnly
     fun tagBundleTest() = runBlocking {
         // Arrange
         val deferred = CompletableDeferred<CameraCaptureResult>()
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt
index 9f8a0df..b1781dd 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt
@@ -32,13 +32,14 @@
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
-import androidx.camera.testing.LabTestRule
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.testutils.assertThrows
 import com.google.common.truth.Truth
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.asExecutor
 import kotlinx.coroutines.runBlocking
@@ -48,8 +49,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.TimeoutException
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
@@ -64,10 +63,6 @@
     @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest()
 
-    // TODO(b/187015621): Remove the rule after the surface can be safely closed.
-    @get:Rule
-    val labTest: LabTestRule = LabTestRule()
-
     @Before
     fun setUp() {
         // TODO(b/162296654): Workaround the google_3a specific behavior.
@@ -112,7 +107,6 @@
     }
 
     @Test
-    @LabTestRule.LabTestOnly
     fun setExposure_futureResultTest() {
         val exposureState = camera.cameraInfo.exposureState
         Assume.assumeTrue(exposureState.isExposureCompensationSupported)
@@ -133,7 +127,6 @@
     }
 
     @Test
-    @LabTestRule.LabTestOnly
     fun setExposureTest() = runBlocking {
         val exposureState = camera.cameraInfo.exposureState
         Assume.assumeTrue(exposureState.isExposureCompensationSupported)
@@ -149,7 +142,6 @@
     }
 
     @Test
-    @LabTestRule.LabTestOnly
     fun setExposureTest_runTwice() = runBlocking {
         val exposureState = camera.cameraInfo.exposureState
         Assume.assumeTrue(exposureState.isExposureCompensationSupported)
@@ -169,7 +161,6 @@
     }
 
     @Test
-    @LabTestRule.LabTestOnly
     fun setExposureAndZoomRatio_theExposureSettingShouldApply() = runBlocking {
         val exposureState = camera.cameraInfo.exposureState
         Assume.assumeTrue(exposureState.isExposureCompensationSupported)
@@ -192,7 +183,6 @@
     }
 
     @Test
-    @LabTestRule.LabTestOnly
     fun setExposureAndLinearZoom_theExposureSettingShouldApply() = runBlocking {
         val exposureState = camera.cameraInfo.exposureState
         Assume.assumeTrue(exposureState.isExposureCompensationSupported)
@@ -210,7 +200,6 @@
     }
 
     @Test
-    @LabTestRule.LabTestOnly
     fun setExposureAndFlash_theExposureSettingShouldApply() = runBlocking {
         val exposureState = camera.cameraInfo.exposureState
         Assume.assumeTrue(exposureState.isExposureCompensationSupported)
@@ -228,7 +217,6 @@
     }
 
     @Test
-    @LabTestRule.LabTestOnly
     fun setExposureTimeout_theNextCallShouldWork() = runBlocking {
         val exposureState = camera.cameraInfo.exposureState
         Assume.assumeTrue(exposureState.isExposureCompensationSupported)
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 185bcad..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].
@@ -105,9 +103,8 @@
 
     override fun startFocusAndMetering(
         action: FocusMeteringAction
-    ): ListenableFuture<FocusMeteringResult> {
-        return focusMeteringControl.startFocusAndMetering(action)
-    }
+    ): ListenableFuture<FocusMeteringResult> =
+        Futures.nonCancellationPropagating(focusMeteringControl.startFocusAndMetering(action))
 
     override fun cancelFocusAndMetering(): ListenableFuture<Void> {
         return Futures.nonCancellationPropagating(
@@ -119,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 8f85b20..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
@@ -27,12 +27,14 @@
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
 import androidx.camera.camera2.pipe.integration.config.CameraScope
-import androidx.camera.core.CameraControl
+import androidx.camera.core.CameraControl.OperationCanceledException
 import androidx.camera.core.FocusMeteringAction
 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,11 @@
 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
 
 /**
  * Implementation of focus and metering controls exposed by [CameraControlInternal].
@@ -52,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)
                 }
             }
         }
@@ -74,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
@@ -89,41 +113,85 @@
         cameraProperties.metadata.getOrDefault(CameraCharacteristics.CONTROL_MAX_REGIONS_AE, 0)
     private val maxAwbRegionCount =
         cameraProperties.metadata.getOrDefault(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB, 0)
+    private var updateSignal: CompletableDeferred<FocusMeteringResult>? = null
+    private var cancelSignal: CompletableDeferred<Result3A?>? = null
 
     fun startFocusAndMetering(
-        action: FocusMeteringAction
+        action: FocusMeteringAction,
+        autoFocusTimeoutMs: Long = AUTO_FOCUS_TIMEOUT_DURATION,
     ): ListenableFuture<FocusMeteringResult> {
         val signal = CompletableDeferred<FocusMeteringResult>()
 
         useCaseCamera?.let { useCaseCamera ->
-            threads.sequentialScope.launch {
-                signal.complete(
+            val job = threads.sequentialScope.launch {
+                cancelSignal?.setCancelException("Cancelled by another startFocusAndMetering()")
+                updateSignal?.setCancelException("Cancelled by another startFocusAndMetering()")
+                updateSignal = signal
+
+                val aeRectangles = meteringRegionsFromMeteringPoints(
+                    action.meteringPointsAe,
+                    maxAeRegionCount,
+                    sensorRect,
+                    defaultAspectRatio
+                )
+                val afRectangles = meteringRegionsFromMeteringPoints(
+                    action.meteringPointsAf,
+                    maxAfRegionCount,
+                    sensorRect,
+                    defaultAspectRatio
+                )
+                val awbRectangles = meteringRegionsFromMeteringPoints(
+                    action.meteringPointsAwb,
+                    maxAwbRegionCount,
+                    sensorRect,
+                    defaultAspectRatio
+                )
+                if (aeRectangles.isEmpty() && afRectangles.isEmpty() && awbRectangles.isEmpty()) {
+                    signal.completeExceptionally(
+                        IllegalArgumentException(
+                            "None of the specified AF/AE/AWB MeteringPoints is supported on" +
+                                " this camera."
+                        )
+                    )
+                    return@launch
+                }
+                val (isCancelEnabled, timeout) = if (action.isAutoCancelEnabled &&
+                    action.autoCancelDurationInMillis < autoFocusTimeoutMs
+                ) {
+                    (true to action.autoCancelDurationInMillis)
+                } else {
+                    (false to autoFocusTimeoutMs)
+                }
+                withTimeoutOrNull(timeout) {
                     useCaseCamera.requestControl.startFocusAndMeteringAsync(
-                        aeRegions = meteringRegionsFromMeteringPoints(
-                            action.meteringPointsAe,
-                            maxAeRegionCount,
-                            sensorRect,
-                            defaultAspectRatio
-                        ),
-                        afRegions = meteringRegionsFromMeteringPoints(
-                            action.meteringPointsAf,
-                            maxAfRegionCount,
-                            sensorRect,
-                            defaultAspectRatio
-                        ),
-                        awbRegions = meteringRegionsFromMeteringPoints(
-                            action.meteringPointsAwb,
-                            maxAwbRegionCount,
-                            sensorRect,
-                            defaultAspectRatio
-                        ),
+                        aeRegions = aeRectangles,
+                        afRegions = afRectangles,
+                        awbRegions = awbRectangles,
                         afTriggerStartAeMode = cameraProperties.getSupportedAeMode(AeMode.ON)
                     ).await().toFocusMeteringResult(true)
-                )
+                }.let { focusMeteringResult ->
+                    if (focusMeteringResult != null) {
+                        signal.complete(focusMeteringResult)
+                    } else {
+                        if (isCancelEnabled) {
+                            if (signal.isActive) {
+                                cancelFocusAndMeteringNow(useCaseCamera, signal)
+                            }
+                        } else {
+                            signal.complete(FocusMeteringResult.create(false))
+                        }
+                    }
+                }
+            }
+
+            signal.invokeOnCompletion { throwable ->
+                if (throwable is OperationCanceledException) {
+                    job.cancel()
+                }
             }
         } ?: run {
             signal.completeExceptionally(
-                CameraControl.OperationCanceledException("Camera is not active.")
+                OperationCanceledException("Camera is not active.")
             )
         }
 
@@ -171,8 +239,31 @@
         } else AeMode.OFF
     }
 
-    fun cancelFocusAndMeteringAsync(): Deferred<Unit> {
-        return CompletableDeferred(null)
+    fun cancelFocusAndMeteringAsync(): Deferred<Result3A?> {
+        val signal = CompletableDeferred<Result3A?>()
+        useCaseCamera?.let { useCaseCamera ->
+            threads.sequentialScope.launch {
+                cancelSignal?.setCancelException("Cancelled by another cancelFocusAndMetering()")
+                cancelSignal = signal
+                signal.complete(cancelFocusAndMeteringNow(useCaseCamera, updateSignal))
+            }
+        } ?: run {
+            signal.completeExceptionally(OperationCanceledException("Camera is not active."))
+        }
+
+        return signal
+    }
+
+    private suspend fun cancelFocusAndMeteringNow(
+        useCaseCamera: UseCaseCamera,
+        signalToCancel: CompletableDeferred<FocusMeteringResult>?,
+    ): Result3A {
+        signalToCancel?.setCancelException("Cancelled by cancelFocusAndMetering()")
+        return useCaseCamera.requestControl.cancelFocusAndMeteringAsync().await()
+    }
+
+    private fun <T> CompletableDeferred<T>.setCancelException(message: String) {
+        completeExceptionally(OperationCanceledException(message))
     }
 
     /**
@@ -194,6 +285,7 @@
 
     companion object {
         const val METERING_WEIGHT_DEFAULT = MeteringRectangle.METERING_WEIGHT_MAX
+        const val AUTO_FOCUS_TIMEOUT_DURATION = 5000L
 
         fun meteringRegionsFromMeteringPoints(
             meteringPoints: List<MeteringPoint>,
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/UseCaseCameraRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
index 8410aed..3d9377d 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
@@ -26,13 +26,13 @@
 import androidx.camera.camera2.pipe.AeMode
 import androidx.camera.camera2.pipe.AfMode
 import androidx.camera.camera2.pipe.AwbMode
+import androidx.camera.camera2.pipe.CameraGraph.Constants3A.METERING_REGIONS_DEFAULT
 import androidx.camera.camera2.pipe.Lock3ABehavior
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.RequestTemplate
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.TorchState
-import androidx.camera.camera2.pipe.core.Log
 import androidx.camera.camera2.pipe.integration.adapter.CaptureConfigAdapter
 import androidx.camera.camera2.pipe.integration.config.UseCaseCameraScope
 import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
@@ -45,7 +45,6 @@
 import dagger.Binds
 import dagger.Module
 import javax.inject.Inject
-import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.async
 
@@ -291,9 +290,17 @@
     }
 
     override suspend fun cancelFocusAndMeteringAsync(): Deferred<Result3A> {
-        // TODO(b/205662153): Implement to cancel FocusAndMetering.
-        Log.warn { "TODO: cancelFocusAndMetering is not yet supported" }
-        return CompletableDeferred(Result3A(Result3A.Status.OK))
+        graph.acquireSession().use {
+            it.unlock3A(ae = true, af = true, awb = true)
+        }.await()
+
+        return graph.acquireSession().use {
+            it.update3A(
+                aeRegions = METERING_REGIONS_DEFAULT.asList(),
+                afRegions = METERING_REGIONS_DEFAULT.asList(),
+                awbRegions = METERING_REGIONS_DEFAULT.asList()
+            )
+        }
     }
 
     override suspend fun issueSingleCaptureAsync(
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 e970d28c..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,11 +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
@@ -59,17 +62,26 @@
 import java.util.concurrent.ExecutionException
 import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
-import junit.framework.TestCase
+import kotlinx.coroutines.CompletableDeferred
 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
@@ -98,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
 
@@ -125,13 +153,35 @@
         )
     }
 
+    // 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()
-        fakeRequestControl.focusMeteringResult3A = Result3A(status = Result3A.Status.OK)
+        fakeRequestControl.focusMeteringResult =
+            CompletableDeferred(Result3A(status = Result3A.Status.OK))
         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)
@@ -279,7 +329,9 @@
         // TODO: This will probably throw an invalid argument exception in future instead of
         //  passing the parameters to request control, better to assert the exception then.
 
-        with(fakeRequestControl.focusMeteringCalls.last()) {
+        val meteringRequests = fakeRequestControl.focusMeteringCalls.lastOrNull()
+            ?: FakeUseCaseCameraRequestControl.FocusMeteringParams()
+        with(meteringRequests) {
             assertWithMessage("Wrong number of AE regions").that(aeRegions.size).isEqualTo(0)
             assertWithMessage("Wrong number of AF regions").that(afRegions.size).isEqualTo(0)
             assertWithMessage("Wrong number of AWB regions").that(awbRegions.size).isEqualTo(0)
@@ -302,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
@@ -383,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)
         }
     }
 
@@ -402,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)
         }
     }
 
@@ -423,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)
         }
     }
 
@@ -449,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)
         }
     }
 
@@ -498,11 +602,13 @@
 
     @Test
     fun startFocusMetering_AfLocked_completesWithFocusFalse() {
-        fakeRequestControl.focusMeteringResult3A = Result3A(
-            status = Result3A.Status.OK,
-            frameMetadata = FakeFrameMetadata(
-                extraMetadata = mapOf(
-                    CONTROL_AF_STATE to CONTROL_AF_STATE_FOCUSED_LOCKED
+        fakeRequestControl.focusMeteringResult = CompletableDeferred(
+            Result3A(
+                status = Result3A.Status.OK,
+                frameMetadata = FakeFrameMetadata(
+                    extraMetadata = mapOf(
+                        CONTROL_AF_STATE to CONTROL_AF_STATE_FOCUSED_LOCKED
+                    )
                 )
             )
         )
@@ -515,11 +621,13 @@
 
     @Test
     fun startFocusMetering_AfNotLocked_completesWithFocusFalse() {
-        fakeRequestControl.focusMeteringResult3A = Result3A(
-            status = Result3A.Status.OK,
-            frameMetadata = FakeFrameMetadata(
-                extraMetadata = mapOf(
-                    CONTROL_AF_STATE to CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
+        fakeRequestControl.focusMeteringResult = CompletableDeferred(
+            Result3A(
+                status = Result3A.Status.OK,
+                frameMetadata = FakeFrameMetadata(
+                    extraMetadata = mapOf(
+                        CONTROL_AF_STATE to CONTROL_AF_STATE_NOT_FOCUSED_LOCKED
+                    )
                 )
             )
         )
@@ -533,11 +641,13 @@
     @Test
     @Ignore("b/263323720: When AfState is null, it means AF is not supported")
     fun startFocusMetering_AfStateIsNull_completesWithFocusTrue() {
-        fakeRequestControl.focusMeteringResult3A = Result3A(
-            status = Result3A.Status.OK,
-            frameMetadata = FakeFrameMetadata(
-                extraMetadata = mapOf(
-                    CONTROL_AF_STATE to null
+        fakeRequestControl.focusMeteringResult = CompletableDeferred(
+            Result3A(
+                status = Result3A.Status.OK,
+                frameMetadata = FakeFrameMetadata(
+                    extraMetadata = mapOf(
+                        CONTROL_AF_STATE to null
+                    )
                 )
             )
         )
@@ -566,50 +676,73 @@
         assertFutureFocusCompleted(result, true)
     }
 
+    @MediumTest
     @Test
-    @Ignore("b/205662153")
-    fun startFocusMetering_cancelledBeforeCompletion_failsWithOperationCanceledOperation() {
+    fun startFocusMetering_cancelledBeforeCompletion_failsWithOperationCanceledOperation() =
+        runBlocking {
+        // Arrange. Set a delay CompletableDeferred
+        fakeRequestControl.focusMeteringResult = CompletableDeferred<Result3A>().apply {
+            async(Dispatchers.Default) {
+                delay(500)
+                complete(
+                    Result3A(
+                        status = Result3A.Status.OK,
+                        frameMetadata = FakeFrameMetadata(
+                            extraMetadata = mapOf(
+                                CONTROL_AF_STATE to CONTROL_AF_STATE_FOCUSED_LOCKED
+                            )
+                        )
+                    )
+                )
+            }
+        }
         val action = FocusMeteringAction.Builder(point1).build()
-        val future = focusMeteringControl.startFocusAndMetering(
-            action
-        )
+        val future = focusMeteringControl.startFocusAndMetering(action)
 
-        // TODO: Check if the following is the correct method to call while enabling this test
+        // Act.
         focusMeteringControl.cancelFocusAndMeteringAsync()
 
-        try {
-            future.get()
-            TestCase.fail("The future should fail.")
-        } catch (e: ExecutionException) {
-            assertThat(e.cause)
-                .isInstanceOf(CameraControl.OperationCanceledException::class.java)
-        } catch (e: InterruptedException) {
-            assertThat(e.cause)
-                .isInstanceOf(CameraControl.OperationCanceledException::class.java)
-        }
+        // Assert.
+        assertFutureFailedWithOperationCancellation(future)
     }
 
     @Test
-    @Ignore("b/205662153: Enable when cancelFocusAndMetering implementation is completed")
     fun startThenCancelThenStart_previous2FuturesFailsWithOperationCanceled() {
-        val action = FocusMeteringAction.Builder(point1)
-            .build()
+        // Arrange. Set a never complete CompletableDeferred
+        fakeRequestControl.focusMeteringResult = CompletableDeferred()
+        fakeRequestControl.cancelFocusMeteringResult = CompletableDeferred()
+        val action = FocusMeteringAction.Builder(point1).build()
 
+        // Act.
         val result1 = focusMeteringControl.startFocusAndMetering(action)
-        // TODO: b/205662153
-//        val result2 = focusMeteringControl.cancelFocusAndMetering()
+        val result2 = focusMeteringControl.cancelFocusAndMeteringAsync().asListenableFuture()
         focusMeteringControl.startFocusAndMetering(action)
 
+        // Assert.
         assertFutureFailedWithOperationCancellation(result1)
-        // TODO: b/205662153
-//        assertFutureFailedWithOperationCancellation(result2)
+        assertFutureFailedWithOperationCancellation(result2)
     }
 
+    @MediumTest
     @Test
-    @Ignore("b/205662153: Enable when cancelFocusAndMetering implementation is completed")
-    fun startMultipleActions_allExceptLatestAreCancelled() {
-        val action = FocusMeteringAction.Builder(point1)
-            .build()
+    fun startMultipleActions_allExceptLatestAreCancelled() = runBlocking {
+        // Arrange. Set a delay CompletableDeferred
+        fakeRequestControl.focusMeteringResult = CompletableDeferred<Result3A>().apply {
+            async(Dispatchers.Default) {
+                delay(500)
+                complete(
+                    Result3A(
+                        status = Result3A.Status.OK,
+                        frameMetadata = FakeFrameMetadata(
+                            extraMetadata = mapOf(
+                                CONTROL_AF_STATE to CONTROL_AF_STATE_FOCUSED_LOCKED
+                            )
+                        )
+                    )
+                )
+            }
+        }
+        val action = FocusMeteringAction.Builder(point1).build()
         val result1 = focusMeteringControl.startFocusAndMetering(action)
         val result2 = focusMeteringControl.startFocusAndMetering(action)
         val result3 = focusMeteringControl.startFocusAndMetering(action)
@@ -619,28 +752,32 @@
     }
 
     @Test
-    @Ignore("b/205662153: Enable when cancelFocusAndMetering implementation is completed")
-    fun startFocusMetering_focusedThenCancel_futureStillCompletes() {
-        fakeRequestControl.focusMeteringResult3A = Result3A(
-            status = Result3A.Status.OK,
-            frameMetadata = FakeFrameMetadata(
-                extraMetadata = mapOf(
-                    CONTROL_AF_STATE to CONTROL_AF_STATE_FOCUSED_LOCKED
+    fun startFocusMetering_focusedThenCancel_futureStillCompletes() = runBlocking {
+        // Arrange.
+        fakeRequestControl.focusMeteringResult = CompletableDeferred(
+            Result3A(
+                status = Result3A.Status.OK,
+                frameMetadata = FakeFrameMetadata(
+                    extraMetadata = mapOf(
+                        CONTROL_AF_STATE to CONTROL_AF_STATE_FOCUSED_LOCKED
+                    )
                 )
             )
         )
         val action = FocusMeteringAction.Builder(point1).build()
 
-        val result = focusMeteringControl.startFocusAndMetering(action)
+        val result = focusMeteringControl.startFocusAndMetering(action).apply {
+           get(3, TimeUnit.SECONDS)
+        }
 
-        // cancel it and then ensure the returned ListenableFuture still completes;
-        // TODO: Check if the following is the correct method to call while enabling this test
-        focusMeteringControl.cancelFocusAndMeteringAsync()
+        // Act. Cancel it and then ensure the returned ListenableFuture still completes.
+        focusMeteringControl.cancelFocusAndMeteringAsync().join()
+
+        // Assert.
         assertFutureFocusCompleted(result, true)
     }
 
     @Test
-    @Ignore("aosp/2369189")
     fun startFocusMeteringAFAEAWB_noPointsAreSupported_failFuture() {
         val focusMeteringControl = initFocusMeteringControl(CAMERA_ID_3)
         val action = FocusMeteringAction.Builder(
@@ -658,7 +795,6 @@
     }
 
     @Test
-    @Ignore("aosp/2369189")
     fun startFocusMeteringAEAWB_noPointsAreSupported_failFuture() {
         val focusMeteringControl = initFocusMeteringControl(CAMERA_ID_3)
         val action = FocusMeteringAction.Builder(
@@ -675,7 +811,6 @@
     }
 
     @Test
-    @Ignore("aosp/2369189")
     fun startFocusMeteringAFAWB_noPointsAreSupported_failFuture() {
         val focusMeteringControl = initFocusMeteringControl(CAMERA_ID_3)
         val action = FocusMeteringAction.Builder(
@@ -708,7 +843,6 @@
     }
 
     @Test
-    @Ignore("aosp/2369189")
     fun startFocusMetering_noPointsAreValid_failFuture() {
         val focusMeteringControl = initFocusMeteringControl(CAMERA_ID_0)
 
@@ -753,6 +887,7 @@
         assertThat(focusMeteringControl.isFocusMeteringSupported(action)).isTrue()
     }
 
+    @Ignore("b/266123157")
     @Test
     fun isFocusMeteringSupported_noSupport3ARegion_shouldReturnFalse() {
         val action = FocusMeteringAction.Builder(point1).build()
@@ -775,18 +910,130 @@
         assertThat(focusMeteringControl.isFocusMeteringSupported(action)).isFalse()
     }
 
+    @Test
+    fun cancelFocusMetering_actionIsCanceledAndFutureCompletes() {
+        // Arrange. Set a never complete CompletableDeferred
+        fakeRequestControl.focusMeteringResult = CompletableDeferred()
+        val action = FocusMeteringAction.Builder(point1).build()
+
+        // Act.
+        val actionResult = focusMeteringControl.startFocusAndMetering(action)
+        val cancelResult = focusMeteringControl.cancelFocusAndMeteringAsync().asListenableFuture()
+
+        // Assert.
+        assertFutureFailedWithOperationCancellation(actionResult)
+        assertThat(cancelResult[3, TimeUnit.SECONDS]?.status).isEqualTo(Result3A.Status.OK)
+    }
+
+    @MediumTest
+    @Test
+    fun cancelFocusAndMetering_autoCancelIsDisabled(): Unit = runBlocking {
+        // Arrange. Set a never complete CompletableDeferred
+        fakeRequestControl.focusMeteringResult = CompletableDeferred()
+        val autoCancelDuration: Long = 500
+        val action = FocusMeteringAction.Builder(point1)
+            .setAutoCancelDuration(autoCancelDuration, TimeUnit.MILLISECONDS)
+            .build()
+        val autoFocusTimeoutDuration: Long = 1000
+        focusMeteringControl.startFocusAndMetering(action, autoFocusTimeoutDuration)
+
+        // Act. Call cancel before the auto cancel occur.
+        focusMeteringControl.cancelFocusAndMeteringAsync().await()
+        assertThat(fakeRequestControl.cancelFocusMeteringCallCount).isEqualTo(1)
+
+        // Assert. cancelFocusMetering only be invoked once.
+        delay(autoFocusTimeoutDuration)
+        assertThat(fakeRequestControl.cancelFocusMeteringCallCount).isEqualTo(1)
+    }
+
+    @MediumTest
+    @Test
+    fun autoCancelDuration_completeWithIsFocusSuccessfulFalse() {
+        // Arrange. Set a never complete CompletableDeferred
+        fakeRequestControl.focusMeteringResult = CompletableDeferred()
+        val autoCancelTimeOutDuration: Long = 500
+        val action = FocusMeteringAction.Builder(point1)
+            .setAutoCancelDuration(autoCancelTimeOutDuration, TimeUnit.MILLISECONDS)
+            .build()
+
+        // Act.
+        val future = focusMeteringControl.startFocusAndMetering(
+            action,
+            autoCancelTimeOutDuration
+        )
+
+        // Assert.
+        assertFutureFocusCompleted(future, false)
+    }
+
+    @MediumTest
+    @Test
+    fun shorterAutoCancelDuration_cancelIsCalled_completeActionFutureIsNotCalled(): Unit =
+        runBlocking {
+            // Arrange. Set a never complete CompletableDeferred
+            fakeRequestControl.focusMeteringResult = CompletableDeferred()
+            val autoCancelDuration: Long = 500
+            val action = FocusMeteringAction.Builder(point1)
+                .setAutoCancelDuration(autoCancelDuration, TimeUnit.MILLISECONDS)
+                .build()
+            val autoFocusTimeoutDuration: Long = 1000
+            focusMeteringControl.startFocusAndMetering(action, autoFocusTimeoutDuration)
+
+            // Act.
+            val future = focusMeteringControl.startFocusAndMetering(
+                action,
+                autoFocusTimeoutDuration
+            )
+
+            // Assert.
+            assertFutureFailedWithOperationCancellation(future)
+        }
+
+    @MediumTest
+    @Test
+    fun longerAutoCancelDuration_completeWithIsFocusSuccessfulFalse() {
+        // Arrange. Set a never complete CompletableDeferred
+        fakeRequestControl.focusMeteringResult = CompletableDeferred()
+        val autoCancelDuration: Long = 1000
+        val action = FocusMeteringAction.Builder(point1)
+            .setAutoCancelDuration(autoCancelDuration, TimeUnit.MILLISECONDS)
+            .build()
+        val autoFocusTimeoutDuration: Long = 500
+
+        // Act.
+        val future = focusMeteringControl.startFocusAndMetering(action, autoFocusTimeoutDuration)
+
+        // Assert.
+        assertFutureFocusCompleted(future, false)
+    }
+
+    @MediumTest
+    @Test
+    fun autoCancelDurationDisabled_completeAfterAutoFocusTimeoutDuration(): Unit = runBlocking {
+        // Arrange. Set a never complete CompletableDeferred
+        fakeRequestControl.focusMeteringResult = CompletableDeferred()
+        val autoCancelDuration: Long = 500
+        val action = FocusMeteringAction.Builder(point1)
+            .setAutoCancelDuration(autoCancelDuration, TimeUnit.MILLISECONDS)
+            .disableAutoCancel()
+            .build()
+        val autoFocusTimeoutTestDuration: Long = 1000
+
+        // Act.
+        val future = focusMeteringControl.startFocusAndMetering(
+            action, autoFocusTimeoutTestDuration
+        )
+
+        // Assert.
+        assertFutureFocusCompleted(future, false)
+    }
+
     // TODO: Port the following tests once their corresponding logics have been implemented.
     //  - [b/255679866] triggerAfWithTemplate, triggerAePrecaptureWithTemplate,
     //          cancelAfAeTriggerWithTemplate
     //  - startFocusAndMetering_AfRegionCorrectedByQuirk
     //  - [b/262225455] cropRegionIsSet_resultBasedOnCropRegion
-    //  - [b/205662153] autoCancelDuration_completeWithIsFocusSuccessfulFalse,
-    //      shorterAutoCancelDuration_cancelIsCalled_completeActionFutureIsNotCalled,
-    //      longerAutoCancelDuration_cancelIsCalled_afterCompleteWithIsFocusSuccessfulFalse,
-    //      autoCancelDurationDisabled_completeAfterAutoFocusTimeoutDuration
     //  The following ones will depend on how exactly they will be implemented.
-    //  - [b/205662153] cancelFocusAndMetering_* (probably many of these tests will no longer be
-    //      applicable in this level since Controller3A handles things a bit differently)
     //  - [b/264018162] addFocusMeteringOptions_hasCorrectAfMode,
     //                  startFocusMetering_isAfAutoModeIsTrue,
     //                  startFocusMetering_AfNotInvolved_isAfAutoModeIsSet,
@@ -887,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
@@ -997,7 +1245,7 @@
         )
     }
 
-    private fun createPreview(suggestedResolution: Size) =
+    private fun createPreview(suggestedStreamSpecResolution: Size) =
         Preview.Builder()
             .setCaptureOptionUnpacker { _, _ -> }
             .setSessionOptionUnpacker() { _, _ -> }
@@ -1008,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/FakeUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
index d318cbe..32977b3 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
@@ -92,7 +92,9 @@
     }
 
     val focusMeteringCalls = mutableListOf<FocusMeteringParams>()
-    var focusMeteringResult3A = Result3A(status = Result3A.Status.OK)
+    var focusMeteringResult = CompletableDeferred(Result3A(status = Result3A.Status.OK))
+    var cancelFocusMeteringCallCount = 0
+    var cancelFocusMeteringResult = CompletableDeferred(Result3A(status = Result3A.Status.OK))
 
     override suspend fun startFocusAndMeteringAsync(
         aeRegions: List<MeteringRectangle>,
@@ -103,11 +105,12 @@
         focusMeteringCalls.add(
             FocusMeteringParams(aeRegions, afRegions, awbRegions, afTriggerStartAeMode)
         )
-        return CompletableDeferred(focusMeteringResult3A)
+        return focusMeteringResult
     }
 
     override suspend fun cancelFocusAndMeteringAsync(): Deferred<Result3A> {
-        return CompletableDeferred(Result3A(status = Result3A.Status.OK))
+        cancelFocusMeteringCallCount++
+        return cancelFocusMeteringResult
     }
 
     override suspend fun issueSingleCaptureAsync(
@@ -120,10 +123,10 @@
     }
 
     data class FocusMeteringParams(
-        val aeRegions: List<MeteringRectangle>,
-        val afRegions: List<MeteringRectangle>,
-        val awbRegions: List<MeteringRectangle>,
-        val afTriggerStartAeMode: AeMode?
+        val aeRegions: List<MeteringRectangle> = emptyList(),
+        val afRegions: List<MeteringRectangle> = emptyList(),
+        val awbRegions: List<MeteringRectangle> = emptyList(),
+        val afTriggerStartAeMode: AeMode? = null
     )
 }
 
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-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessorTest.kt
index 401ad8d..6435358 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessorTest.kt
@@ -43,6 +43,7 @@
 import androidx.camera.testing.CameraUtil
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.testutils.assertThrows
@@ -339,6 +340,7 @@
         }
     }
 
+    @FlakyTest(bugId = 265008341)
     @Test
     fun canCloseBeforeJpegConversion(): Unit = runBlocking {
         withTimeout(3000) {
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
index 14bab15..7f8a579 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCamera.java
@@ -79,6 +79,10 @@
                 new FakeCameraInfoInternal(DEFAULT_CAMERA_ID));
     }
 
+    public FakeCamera(@NonNull CameraControlInternal cameraControl) {
+        this(DEFAULT_CAMERA_ID, cameraControl, new FakeCameraInfoInternal(DEFAULT_CAMERA_ID));
+    }
+
     public FakeCamera(@NonNull String cameraId) {
         this(cameraId, /*cameraControl=*/null, new FakeCameraInfoInternal(cameraId));
     }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
index 0d2a5cb..c032e08 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
@@ -52,6 +52,19 @@
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public final class FakeCameraControl implements CameraControlInternal {
     private static final String TAG = "FakeCameraControl";
+    private static final ControlUpdateCallback NO_OP_CALLBACK = new ControlUpdateCallback() {
+        @Override
+        public void onCameraControlUpdateSessionConfig() {
+            // No-op
+        }
+
+        @Override
+        public void onCameraControlCaptureRequests(
+                @NonNull List<CaptureConfig> captureConfigs) {
+            // No-op
+        }
+    };
+
     private final ControlUpdateCallback mControlUpdateCallback;
     private final SessionConfig.Builder mSessionConfigBuilder = new SessionConfig.Builder();
     @ImageCapture.FlashMode
@@ -64,6 +77,13 @@
 
     private boolean mIsZslDisabledByUseCaseConfig = false;
     private boolean mIsZslConfigAdded = false;
+    private float mZoomRatio = -1;
+    private float mLinearZoom = -1;
+    private boolean mTorchEnabled = false;
+
+    public FakeCameraControl() {
+        this(NO_OP_CALLBACK);
+    }
 
     public FakeCameraControl(@NonNull ControlUpdateCallback controlUpdateCallback) {
         mControlUpdateCallback = controlUpdateCallback;
@@ -149,7 +169,6 @@
     /**
      * Checks if {@link FakeCameraControl#addZslConfig(Size, SessionConfig.Builder)} is
      * triggered. Only for testing purpose.
-     * @return
      */
     public boolean isZslConfigAdded() {
         return mIsZslConfigAdded;
@@ -159,9 +178,14 @@
     @NonNull
     public ListenableFuture<Void> enableTorch(boolean torch) {
         Logger.d(TAG, "enableTorch(" + torch + ")");
+        mTorchEnabled = torch;
         return Futures.immediateFuture(null);
     }
 
+    public boolean getTorchEnabled() {
+        return mTorchEnabled;
+    }
+
     @NonNull
     @Override
     public ListenableFuture<Integer> setExposureCompensationIndex(int exposure) {
@@ -222,15 +246,25 @@
     @NonNull
     @Override
     public ListenableFuture<Void> setZoomRatio(float ratio) {
+        mZoomRatio = ratio;
         return Futures.immediateFuture(null);
     }
 
+    public float getZoomRatio() {
+        return mZoomRatio;
+    }
+
     @NonNull
     @Override
     public ListenableFuture<Void> setLinearZoom(float linearZoom) {
+        mLinearZoom = linearZoom;
         return Futures.immediateFuture(null);
     }
 
+    public float getLinearZoom() {
+        return mLinearZoom;
+    }
+
     @Override
     public void addInteropConfig(@NonNull Config config) {
         for (Config.Option<?> option : config.listOptions()) {
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/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/DeviceCompatibilityTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt
index ac508e5..7892f10 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/DeviceCompatibilityTest.kt
@@ -70,6 +70,11 @@
         active = implName == CameraPipeConfig::class.simpleName,
     )
 
+    @get:Rule
+    val cameraRule = CameraUtil.grantCameraPermissionAndPreTest(
+        CameraUtil.PreTestCameraIdList(cameraConfig)
+    )
+
     @Before
     fun setup() {
         CameraXUtil.initialize(context, cameraConfig).get()
@@ -124,6 +129,9 @@
     }
 
     private fun getSupportedProfiles(cameraSelector: CameraSelector): List<CamcorderProfileProxy> {
+        if (!CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!)) {
+            return emptyList()
+        }
         val cameraInfo = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector).cameraInfo
         val videoCapabilities = VideoCapabilities.from(cameraInfo)
         return videoCapabilities.supportedQualities
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
index 380f24b..29c53eb 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
@@ -20,6 +20,7 @@
 import android.Manifest
 import android.annotation.SuppressLint
 import android.app.AppOpsManager
+import android.app.AppOpsManager.OnOpNotedCallback
 import android.app.AsyncNotedAppOp
 import android.app.SyncNotedAppOp
 import android.content.ContentResolver
@@ -41,8 +42,9 @@
 import androidx.camera.core.Preview
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.impl.ImageFormatConstants
-import androidx.camera.core.impl.Observable
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.impl.Observable.Observer
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.testing.AudioUtil
 import androidx.camera.testing.CameraPipeConfigTestRule
@@ -55,18 +57,27 @@
 import androidx.camera.testing.mocks.MockConsumer
 import androidx.camera.testing.mocks.helpers.CallTimes
 import androidx.camera.testing.mocks.helpers.CallTimesAtLeast
+import androidx.camera.video.VideoOutput.SourceState.ACTIVE_NON_STREAMING
+import androidx.camera.video.VideoOutput.SourceState.ACTIVE_STREAMING
+import androidx.camera.video.VideoOutput.SourceState.INACTIVE
+import androidx.camera.video.VideoRecordEvent.Finalize
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INVALID_OUTPUT_OPTIONS
+import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NO_VALID_DATA
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_RECORDER_ERROR
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE
+import androidx.camera.video.VideoRecordEvent.Pause
+import androidx.camera.video.VideoRecordEvent.Resume
+import androidx.camera.video.VideoRecordEvent.Start
+import androidx.camera.video.VideoRecordEvent.Status
 import androidx.camera.video.internal.compat.quirk.DeactivateEncoderSurfaceBeforeStopEncoderQuirk
 import androidx.camera.video.internal.compat.quirk.DeviceQuirks
 import androidx.camera.video.internal.compat.quirk.ExtraSupportedResolutionQuirk
 import androidx.camera.video.internal.compat.quirk.MediaStoreVideoCannotWrite
+import androidx.camera.video.internal.encoder.EncoderFactory
 import androidx.camera.video.internal.encoder.InvalidConfigException
-import androidx.core.util.Consumer
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
@@ -77,9 +88,7 @@
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import java.io.File
-import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executor
-import java.util.concurrent.Semaphore
 import java.util.concurrent.TimeUnit
 import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CompletableDeferred
@@ -104,10 +113,14 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.timeout
 
+private const val DEFAULT_STATUS_COUNT = 5
 private const val GENERAL_TIMEOUT = 5000L
 private const val STATUS_TIMEOUT = 15000L
 private const val TEST_ATTRIBUTION_TAG = "testAttribution"
-private const val BITRATE_AUTO = 0
+// For the file size is small, the final file length possibly exceeds the file size limit
+// after adding the file header. We still add the buffer for the tolerance of comparing the
+// file length and file size limit.
+private const val FILE_SIZE_LIMIT_BUFFER = 50 * 1024 // 50k threshold buffer
 
 @LargeTest
 @RunWith(Parameterized::class)
@@ -156,12 +169,11 @@
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
     private val context: Context = ApplicationProvider.getApplicationContext()
     private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+    private val recordingsToStop = mutableListOf<RecordingProcess>()
 
     private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
-    private lateinit var recorder: Recorder
     private lateinit var preview: Preview
     private lateinit var surfaceTexturePreview: Preview
-    private lateinit var mockVideoRecordEventConsumer: MockConsumer<VideoRecordEvent>
 
     @Before
     fun setUp() {
@@ -249,19 +261,18 @@
             surfaceTexturePreview,
             preview
         )
-
-        mockVideoRecordEventConsumer = MockConsumer<VideoRecordEvent>()
     }
 
     @After
     fun tearDown() {
+        for (recording in recordingsToStop) {
+            recording.stop()
+        }
+
         if (this::cameraUseCaseAdapter.isInitialized) {
             instrumentation.runOnMainSync {
                 cameraUseCaseAdapter.removeUseCases(cameraUseCaseAdapter.useCases)
             }
-            if (this::recorder.isInitialized) {
-                recorder.onSourceStateChanged(VideoOutput.SourceState.INACTIVE)
-            }
         }
 
         CameraXUtil.shutdown().get(10, TimeUnit.SECONDS)
@@ -269,316 +280,207 @@
 
     @Test
     fun canRecordToFile() {
-        testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(BITRATE_AUTO, enableAudio = true)
+        // Arrange.
+        val outputOptions = createFileOutputOptions()
+        val recording = createRecordingProcess(outputOptions = outputOptions)
+
+        // Act.
+        recording.startAndVerify()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            val uri = finalize.outputResults.outputUri
+            assertThat(uri).isEqualTo(Uri.fromFile(outputOptions.file))
+            checkFileHasAudioAndVideo(uri)
+        }
     }
 
     @Test
     fun recordingWithSetTargetVideoEncodingBitRate() {
         testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(6_000_000)
-        verifyConfiguredVideoBitrate()
     }
 
     @Test
     fun recordingWithSetTargetVideoEncodingBitRateOutOfRange() {
         testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(1000_000_000)
-        verifyConfiguredVideoBitrate()
     }
 
     @Test
     fun recordingWithNegativeBitRate() {
-        initializeRecorder()
         assertThrows(IllegalArgumentException::class.java) {
-            Recorder.Builder().setTargetVideoEncodingBitRate(-5).build()
+            createRecorder(targetBitrate = -5)
         }
     }
 
     @Test
     fun canRecordToMediaStore() {
-        initializeRecorder()
         assumeTrue(
             "Ignore the test since the MediaStore.Video has compatibility issues.",
             DeviceQuirks.get(MediaStoreVideoCannotWrite::class.java) == null
         )
-        invokeSurfaceRequest()
-        val statusSemaphore = Semaphore(0)
-        val finalizeSemaphore = Semaphore(0)
-        val context: Context = ApplicationProvider.getApplicationContext()
+
+        // Arrange.
         val contentResolver: ContentResolver = context.contentResolver
         val contentValues = ContentValues().apply {
             put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
         }
-
         val outputOptions = MediaStoreOutputOptions.Builder(
             contentResolver,
             MediaStore.Video.Media.EXTERNAL_CONTENT_URI
         ).setContentValues(contentValues).build()
+        val recording = createRecordingProcess(outputOptions = outputOptions)
 
-        var uri: Uri = Uri.EMPTY
-        val recording = recorder.prepareRecording(context, outputOptions)
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor()) {
-                if (it is VideoRecordEvent.Status) {
-                    statusSemaphore.release()
-                }
-                if (it is VideoRecordEvent.Finalize) {
-                    uri = it.outputResults.outputUri
-                    finalizeSemaphore.release()
-                }
-            }
+        // Act.
+        recording.startAndVerify()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            val uri = finalize.outputResults.outputUri
+            checkFileHasAudioAndVideo(uri)
 
-        assertThat(statusSemaphore.tryAcquire(5, 15000L, TimeUnit.MILLISECONDS)).isTrue()
-
-        recording.stopSafely()
-
-        // Wait for the recording to complete.
-        assertThat(finalizeSemaphore.tryAcquire(GENERAL_TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
-
-        assertThat(uri).isNotEqualTo(Uri.EMPTY)
-
-        checkFileHasAudioAndVideo(uri)
-
-        contentResolver.delete(uri, null, null)
+            // Clean-up.
+            contentResolver.delete(uri, null, null)
+        }
     }
 
     @Test
     @SdkSuppress(minSdkVersion = 26)
     fun canRecordToFileDescriptor() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-        val pfd = ParcelFileDescriptor.open(
-            file,
-            ParcelFileDescriptor.MODE_READ_WRITE
-        )
-        val recording = recorder
-            .prepareRecording(context, FileDescriptorOutputOptions.Builder(pfd).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
+        // Arrange.
+        val file = createTempFile()
+        val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
+        val outputOptions = FileDescriptorOutputOptions.Builder(pfd).build()
+        val recording = createRecordingProcess(outputOptions = outputOptions)
 
+        // Act.
+        recording.startAndVerify()
         // ParcelFileDescriptor should be safe to close after PendingRecording#start.
         pfd.close()
+        recording.stopAndVerify()
 
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        recording.stopSafely()
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true,
-            GENERAL_TIMEOUT
-        )
-
+        // Assert.
         checkFileHasAudioAndVideo(Uri.fromFile(file))
-
-        file.delete()
     }
 
     @Test
     @SdkSuppress(minSdkVersion = 26)
     fun recordToFileDescriptor_withClosedFileDescriptor_receiveError() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val file = createTempFile()
         val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
-
         pfd.close()
+        val outputOptions = FileDescriptorOutputOptions.Builder(pfd).build()
+        val recording = createRecordingProcess(outputOptions = outputOptions)
 
-        recorder.prepareRecording(context, FileDescriptorOutputOptions.Builder(pfd).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        // Check the output Uri from the finalize event match the Uri from the given file.
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
+        // Act.
+        recording.start()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_INVALID_OUTPUT_OPTIONS)
         }
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent::class.java,
-            false,
-            CallTimesAtLeast(1),
-            captor
-        )
-
-        val finalize = captor.value as VideoRecordEvent.Finalize
-        assertThat(finalize.error).isEqualTo(ERROR_INVALID_OUTPUT_OPTIONS)
-
-        file.delete()
     }
 
     @Test
     @SdkSuppress(minSdkVersion = 21, maxSdkVersion = 25)
     @SuppressLint("NewApi") // Intentionally testing behavior of calling from invalid API level
     fun prepareRecordingWithFileDescriptor_throwsExceptionBeforeApi26() {
-        initializeRecorder()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recorder = createRecorder()
+        val file = createTempFile()
         ParcelFileDescriptor.open(
             file,
             ParcelFileDescriptor.MODE_READ_WRITE
         ).use { pfd ->
+            // Assert.
             assertThrows(UnsupportedOperationException::class.java) {
+                // Act.
                 recorder.prepareRecording(context, FileDescriptorOutputOptions.Builder(pfd).build())
             }
         }
-
-        file.delete()
     }
 
     @Test
     fun canPauseResume() {
-        initializeRecorder()
-        invokeSurfaceRequest()
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
-                pause()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Pause::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-
-                resume()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Resume::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-                // Check there are data being encoded after resuming.
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Status::class.java,
-                    true, STATUS_TIMEOUT, CallTimesAtLeast(5)
-                )
-
-                stopSafely()
-            }
-
-        // Wait for the recording to be finalized.
-        mockVideoRecordEventConsumer.verifyAcceptCall(VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT)
-
-        checkFileHasAudioAndVideo(Uri.fromFile(file))
-
-        file.delete()
+        // Act.
+        recording.startAndVerify()
+        recording.pauseAndVerify()
+        recording.resumeAndVerify()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_NONE)
+        }
     }
 
     @Test
     fun canStartRecordingPaused_whenRecorderInitializing() {
-        initializeRecorder()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recorder = createRecorder(sendSurfaceRequest = false)
+        val recording = createRecordingProcess(recorder = recorder)
 
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
-                pause()
+        // Act.
+        recording.start()
+        recording.pause()
+        // Only invoke surface request after pause() has been called
+        recorder.sendSurfaceRequest()
 
-                // Only invoke surface request after pause() has been called
-                invokeSurfaceRequest()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Start::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Pause::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-
-                stopSafely()
-            }
-
-        file.delete()
+        // Assert.
+        recording.verifyStart()
+        recording.verifyPause()
     }
 
     @Test
     fun canReceiveRecordingStats() {
-        initializeRecorder()
-        invokeSurfaceRequest()
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Act.
+        recording.startAndVerify()
+        recording.pauseAndVerify()
+        recording.resumeAndVerify()
+        recording.stopAndVerify()
 
-        // Start
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
-                mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-                pause()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Pause::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-
-                resume()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Resume::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Status::class.java,
-                    true, STATUS_TIMEOUT, CallTimesAtLeast(5)
-                )
-
-                stopSafely()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Finalize::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-            }
-
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
-        }
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent::class.java,
-            false,
-            CallTimesAtLeast(1),
-            captor
+        // Assert.
+        val events = recording.getAllEvents()
+        assertThat(events.size).isAtLeast(
+            1 /* Start */ +
+                5 /* Status */ +
+                1 /* Pause */ +
+                1 /* Resume */ +
+                5 /* Status */ +
+                1 /* Stop */
         )
 
-        captor.allValues.run {
-            assertThat(size).isAtLeast(
-                (
-                    1 /* Start */ +
-                        5 /* Status */ +
-                        1 /* Pause */ +
-                        1 /* Resume */ +
-                        5 /* Status */ +
-                        1 /* Stop */
-                    )
-            )
-
-            // Ensure duration and bytes are increasing
-            take(size - 1).mapIndexed { index, _ ->
-                Pair(get(index).recordingStats, get(index + 1).recordingStats)
-            }.forEach { (former: RecordingStats, latter: RecordingStats) ->
-                assertThat(former.numBytesRecorded).isAtMost(latter.numBytesRecorded)
-                assertThat(former.recordedDurationNanos).isAtMost((latter.recordedDurationNanos))
-            }
-
-            // Ensure they are not all zero by checking last stats
-            last().recordingStats.also {
-                assertThat(it.numBytesRecorded).isGreaterThan(0L)
-                assertThat(it.recordedDurationNanos).isGreaterThan(0L)
-            }
+        // Assert: Ensure duration and bytes are increasing.
+        List(events.size - 1) { index ->
+            Pair(events[index].recordingStats, events[index + 1].recordingStats)
+        }.forEach { (former: RecordingStats, latter: RecordingStats) ->
+            assertThat(former.numBytesRecorded).isAtMost(latter.numBytesRecorded)
+            assertThat(former.recordedDurationNanos).isAtMost((latter.recordedDurationNanos))
         }
 
-        file.delete()
+        // Assert: Ensure they are not all zero by checking the last stats.
+        events.last().recordingStats.also {
+            assertThat(it.numBytesRecorded).isGreaterThan(0L)
+            assertThat(it.recordedDurationNanos).isGreaterThan(0L)
+        }
     }
 
     @Test
     fun setFileSizeLimit() {
-        initializeRecorder()
+        // Arrange.
         val fileSizeLimit = 500L * 1024L // 500 KB
-        runFileSizeLimitTest(fileSizeLimit)
+        val outputOptions = createFileOutputOptions(fileSizeLimit = fileSizeLimit)
+        val recording = createRecordingProcess(outputOptions = outputOptions)
+
+        // Act.
+        recording.startAndVerify()
+        recording.verifyFinalize(timeoutMs = 60_000L) { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_FILE_SIZE_LIMIT_REACHED)
+            assertThat(outputOptions.file.length())
+                .isLessThan(fileSizeLimit + FILE_SIZE_LIMIT_BUFFER)
+        }
     }
 
     // Sets the file size limit to 1 byte, which will be lower than the initial data sent from
@@ -586,800 +488,429 @@
     // written to it.
     @Test
     fun setFileSizeLimitLowerThanInitialDataSize() {
-        initializeRecorder()
+        // Arrange.
         val fileSizeLimit = 1L // 1 byte
-        runFileSizeLimitTest(fileSizeLimit)
+        val outputOptions = createFileOutputOptions(fileSizeLimit = fileSizeLimit)
+        val recording = createRecordingProcess(outputOptions = outputOptions)
+
+        // Act.
+        recording.start()
+        recording.verifyFinalize { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_FILE_SIZE_LIMIT_REACHED)
+        }
     }
 
     @Test
     fun setLocation() {
-        initializeRecorder()
         runLocationTest(createLocation(25.033267462243586, 121.56454121737946))
     }
 
     @Test
     fun setNegativeLocation() {
-        initializeRecorder()
         runLocationTest(createLocation(-27.14394722411734, -109.33053675296067))
     }
 
     @Test
     fun stop_withErrorWhenDurationLimitReached() {
-        initializeRecorder()
-        val videoRecordEventListener = MockConsumer<VideoRecordEvent>()
-        invokeSurfaceRequest()
+        // Arrange.
         val durationLimitMs = 3000L
-        val durationTolerance = 50L
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-        val outputOptions = FileOutputOptions.Builder(file)
-            .setDurationLimitMillis(durationLimitMs)
-            .build()
+        val durationToleranceMs = 50L
+        val outputOptions = createFileOutputOptions(durationLimitMillis = durationLimitMs)
+        val recording = createRecordingProcess(outputOptions = outputOptions)
 
-        val recording = recorder
-            .prepareRecording(context, outputOptions)
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), videoRecordEventListener)
+        // Act.
+        recording.start()
 
-        // The recording should be finalized after the specified duration limit plus some time
-        // for processing it.
-        videoRecordEventListener.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            false,
-            durationLimitMs + 2000L
-        )
-
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> {
-                argument -> VideoRecordEvent::class.java.isInstance(argument)
+        // Assert.
+        recording.verifyFinalize(timeoutMs = durationLimitMs + 2000L) { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_DURATION_LIMIT_REACHED)
+            assertThat(finalize.recordingStats.recordedDurationNanos)
+                .isAtMost(TimeUnit.MILLISECONDS.toNanos(durationLimitMs + durationToleranceMs))
+            checkDurationAtMost(
+                Uri.fromFile(outputOptions.file),
+                durationLimitMs + durationToleranceMs
+            )
         }
-        videoRecordEventListener.verifyAcceptCall(VideoRecordEvent::class.java,
-            false, CallTimesAtLeast(1), captor)
-
-        val finalize = captor.value as VideoRecordEvent.Finalize
-        assertThat(finalize.error).isEqualTo(ERROR_DURATION_LIMIT_REACHED)
-        assertThat(finalize.recordingStats.recordedDurationNanos)
-            .isAtMost(TimeUnit.MILLISECONDS.toNanos(durationLimitMs + durationTolerance))
-        checkDurationAtMost(Uri.fromFile(file), durationLimitMs)
-
-        recording.stopSafely()
-        file.delete()
     }
 
     @Test
     fun checkStreamState() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
+        // Arrange.
+        val recorder = createRecorder()
         @Suppress("UNCHECKED_CAST")
-        val streamInfoObserver =
-            mock(Observable.Observer::class.java) as Observable.Observer<StreamInfo>
+        val streamInfoObserver = mock(Observer::class.java) as Observer<StreamInfo>
         val inOrder = inOrder(streamInfoObserver)
-        recorder.streamInfo.addObserver(CameraXExecutors.directExecutor(), streamInfoObserver)
+        recorder.streamInfo.addObserver(directExecutor(), streamInfoObserver)
 
-        // Recorder should start in INACTIVE stream state before any recordings
-        inOrder.verify(streamInfoObserver, timeout(5000L)).onNewData(
-            argThat {
-                it!!.streamState == StreamInfo.StreamState.INACTIVE
-            }
-        )
-
-        // Start
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-        // Starting recording should move Recorder to ACTIVE stream state
-        inOrder.verify(streamInfoObserver, timeout(5000L)).onNewData(
-            argThat {
-                it!!.streamState == StreamInfo.StreamState.ACTIVE
-            }
-        )
-
-        recording.stopSafely()
-
-        // Stopping recording should eventually move to INACTIVE stream state
+        // Assert: Recorder should start in INACTIVE stream state before any recordings
         inOrder.verify(streamInfoObserver, timeout(GENERAL_TIMEOUT)).onNewData(
             argThat {
                 it!!.streamState == StreamInfo.StreamState.INACTIVE
             }
         )
+        val recording = createRecordingProcess(recorder = recorder)
 
-        file.delete()
+        // Act.
+        recording.start()
+
+        // Assert: Starting recording should move Recorder to ACTIVE stream state
+        inOrder.verify(streamInfoObserver, timeout(5000L)).onNewData(
+            argThat { it!!.streamState == StreamInfo.StreamState.ACTIVE }
+        )
+
+        // Act.
+        recording.stop()
+
+        // Assert: Stopping recording should eventually move to INACTIVE stream state
+        inOrder.verify(streamInfoObserver, timeout(GENERAL_TIMEOUT)).onNewData(
+            argThat {
+                it!!.streamState == StreamInfo.StreamState.INACTIVE
+            }
+        )
     }
 
     @Test
     fun start_throwsExceptionWhenActive() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-        val outputOptions = FileOutputOptions.Builder(file).build()
+        // Arrange.
+        val recorder = createRecorder()
+        val recording = createRecordingProcess(recorder = recorder)
 
-        val recording = recorder.prepareRecording(context, outputOptions).start(
-            CameraXExecutors.directExecutor()
-        ) {}
+        // Act: 1st start.
+        recording.start()
 
-        val pendingRecording = recorder.prepareRecording(context, outputOptions)
+        // Assert.
         assertThrows(java.lang.IllegalStateException::class.java) {
-            pendingRecording.start(CameraXExecutors.directExecutor()) {}
+            // Act: 2nd start.
+            val recording2 = createRecordingProcess(recorder = recorder)
+            recording2.start()
         }
-
-        recording.close()
-        file.delete()
     }
 
     @Test
     fun start_whenSourceActiveNonStreaming() {
-        initializeRecorder()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recorder = createRecorder(initSourceState = ACTIVE_NON_STREAMING)
+        val recording = createRecordingProcess(recorder = recorder)
 
-        recorder.onSourceStateChanged(VideoOutput.SourceState.ACTIVE_NON_STREAMING)
-
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        invokeSurfaceRequest()
-
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        recording.stopSafely()
-        // Wait for the recording to be finalized.
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-
-        file.delete()
+        // Act.
+        recording.start()
+        recorder.onSourceStateChanged(ACTIVE_STREAMING)
+        recording.verifyStart()
+        recording.verifyStatus()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_NONE)
+        }
     }
 
     @Test
     fun start_finalizeImmediatelyWhenSourceInactive() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val videoCaptureMonitor = VideoCaptureMonitor()
-        recorder.startVideoRecording(temporaryFolder.newFile(), videoCaptureMonitor).use {
-            // Ensure the Recorder is initialized before start test.
-            videoCaptureMonitor.waitForVideoCaptureStatus()
+        // Arrange.
+        val recorder = createRecorder(initSourceState = INACTIVE)
+        val recording = createRecordingProcess(recorder = recorder)
+
+        // Act.
+        recording.start()
+
+        // Assert.
+        recording.verifyFinalize { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_SOURCE_INACTIVE)
         }
-
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
-        recorder.onSourceStateChanged(VideoOutput.SourceState.INACTIVE)
-
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            false, GENERAL_TIMEOUT
-        )
-        mockVideoRecordEventConsumer.verifyNoMoreAcceptCalls(false)
-
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
-        }
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent::class.java,
-            false,
-            CallTimesAtLeast(1),
-            captor
-        )
-
-        val finalize = captor.value as VideoRecordEvent.Finalize
-        assertThat(finalize.error).isEqualTo(ERROR_SOURCE_INACTIVE)
-
-        recording.stopSafely()
-
-        file.delete()
     }
 
     @Test
     fun pause_whenSourceActiveNonStreaming() {
-        initializeRecorder()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
-        recorder.onSourceStateChanged(VideoOutput.SourceState.ACTIVE_NON_STREAMING)
-
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
-                pause()
-
-                invokeSurfaceRequest()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Start::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Pause::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-
-                stopSafely()
-            }
-
-        // Wait for the recording to be finalized.
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
+        // Arrange.
+        val recorder = createRecorder(
+            sendSurfaceRequest = false,
+            initSourceState = ACTIVE_NON_STREAMING
         )
+        val recording = createRecordingProcess(recorder = recorder)
 
-        // If the recording is paused immediately after being started, the recording should be
-        // finalized with ERROR_NO_VALID_DATA.
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
+        // Act.
+        recording.start()
+        recording.pause()
+        recorder.sendSurfaceRequest()
+
+        // Assert.
+        recording.verifyStart()
+        recording.verifyPause()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            assertThat(finalize.error).isEqualTo(ERROR_NO_VALID_DATA)
         }
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent::class.java,
-            false,
-            CallTimesAtLeast(1),
-            captor
-        )
-
-        val finalize = captor.value as VideoRecordEvent.Finalize
-        assertThat(finalize.error).isEqualTo(ERROR_NO_VALID_DATA)
-
-        file.delete()
     }
 
     @Test
     fun pause_noOpWhenAlreadyPaused() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
-                mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
+        // Act.
+        recording.startAndVerify()
+        recording.pauseAndVerify()
+        recording.pause()
 
-                pause()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Pause::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-
-                pause()
-
-                stopSafely()
-            }
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-
-        // As described in b/197416199, there might be encoded data in flight which will trigger
-        // Status event after pausing. So here it checks there's only one Pause event.
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
-        }
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent::class.java,
-            false,
-            CallTimesAtLeast(1),
-            captor
-        )
-
-        assertThat(captor.allValues.count { it is VideoRecordEvent.Pause }).isAtMost(1)
-
-        file.delete()
+        // Assert: One Pause event.
+        val events = recording.getAllEvents()
+        val pauseEvents = events.filterIsInstance<Pause>()
+        assertThat(pauseEvents.size).isAtMost(1)
     }
 
     @Test
     fun pause_throwsExceptionWhenStopping() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
+        // Act.
+        recording.startAndVerify()
+        recording.stopAndVerify()
 
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        recording.stopSafely()
-
+        // Assert.
         assertThrows(IllegalStateException::class.java) {
             recording.pause()
         }
-
-        file.delete()
     }
 
     @Test
     fun resume_noOpWhenNotPaused() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        // Calling resume shouldn't affect the stream of status events finally followed
-        // by a finalize event. There shouldn't be another resume event generated.
+        // Act.
+        recording.startAndVerify()
         recording.resume()
+        recording.stopAndVerify()
 
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Status::class.java,
-            true,
-            STATUS_TIMEOUT,
-            CallTimesAtLeast(5)
-        )
-
-        recording.stopSafely()
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-
-        // Ensure no resume events were ever sent.
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Resume::class.java,
-            false,
-            GENERAL_TIMEOUT,
-            CallTimes(0)
-        )
-
-        file.delete()
+        // Assert: No Resume event.
+        val events = recording.getAllEvents()
+        val resumeEvents = events.filterIsInstance<Resume>()
+        assertThat(resumeEvents).isEmpty()
     }
 
     @Test
     fun resume_throwsExceptionWhenStopping() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
+        // Act.
+        recording.startAndVerify()
+        recording.stop()
 
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        recording.stopSafely()
-
+        // Assert.
         assertThrows(IllegalStateException::class.java) {
-            recording.resume()
+            recording.resumeAndVerify()
         }
-
-        file.delete()
     }
 
     @Test
     fun stop_beforeSurfaceRequested() {
-        initializeRecorder()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recorder = createRecorder(sendSurfaceRequest = false)
+        val recording = createRecordingProcess(recorder = recorder)
 
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
+        // Act.
+        recording.start()
+        recording.stop()
+        recorder.sendSurfaceRequest()
 
-        recording.pause()
-
-        recording.stopSafely()
-
-        invokeSurfaceRequest()
-
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
+        // Assert.
+        recording.verifyFinalize { finalize ->
+            assertThat(finalize.error).isEqualTo(ERROR_NO_VALID_DATA)
         }
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent::class.java,
-            false,
-            CallTimesAtLeast(1),
-            captor
-        )
-
-        val finalize = captor.value as VideoRecordEvent.Finalize
-        assertThat(finalize.error).isEqualTo(ERROR_NO_VALID_DATA)
-
-        file.delete()
-    }
-
-    @Test
-    fun stop_fromAutoCloseable() {
-        initializeRecorder()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
-        // Recording will be stopped by AutoCloseable.close() upon exiting use{} block
-        val pendingRecording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-        pendingRecording.start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-            .use {
-                invokeSurfaceRequest()
-                mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-            }
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-
-        file.delete()
     }
 
     @Test
     fun stop_WhenUseCaseDetached() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .withAudioEnabled()
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
+        // Act.
+        recording.startAndVerify()
         instrumentation.runOnMainSync {
             cameraUseCaseAdapter.removeUseCases(listOf(preview))
         }
 
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-
-        recording.stopSafely()
-        file.delete()
+        // Assert.
+        recording.verifyFinalize { finalize ->
+            assertThat(finalize.error).isEqualTo(ERROR_SOURCE_INACTIVE)
+        }
     }
 
-    @Suppress("UNUSED_VALUE", "ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
     @Test
     fun stop_whenRecordingIsGarbageCollected() {
-        initializeRecorder()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        var recording: RecordingProcess? = createRecordingProcess()
+        val listener = recording!!.listener
 
-        var recording: Recording? = recorder
-            .prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        // First ensure the recording gets some status events
-        invokeSurfaceRequest()
-
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
+        // Act.
+        recording.startAndVerify()
         // Remove reference to recording and run GC. The recording should be stopped once
         // the Recording's finalizer runs.
+        recordingsToStop.remove(recording)
+        @Suppress("UNUSED_VALUE")
         recording = null
         GarbageCollectionUtil.runFinalization()
 
-        // Ensure the event listener gets a finalize event. Note: the word "finalize" is very
-        // overloaded here. This event means the recording has finished, but does not relate to the
-        // finalizer that runs during garbage collection. However, that is what causes the
+        // Assert: Ensure the event listener gets a finalize event. Note: the word "finalize" is
+        // very overloaded here. This event means the recording has finished, but does not relate
+        // to the finalizer that runs during garbage collection. However, that is what causes the
         // recording to finish.
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-
-        file.delete()
+        listener.verifyFinalize()
     }
 
     @Test
     fun stop_noOpWhenStopping() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recording = createRecordingProcess()
 
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).apply {
-                mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
+        // Act.
+        recording.startAndVerify()
+        recording.stopAndVerify()
+        recording.stop()
 
-                stopSafely()
-                stopSafely()
-            }
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-        mockVideoRecordEventConsumer.verifyNoMoreAcceptCalls(true)
-
-        file.delete()
+        // Assert.
+        recording.verifyNoMoreEvent()
     }
 
     @Test
     fun optionsOverridesDefaults() {
-        initializeRecorder()
         val qualitySelector = QualitySelector.from(Quality.HIGHEST)
-        val recorder = Recorder.Builder()
-            .setQualitySelector(qualitySelector)
-            .build()
+        val recorder = createRecorder(qualitySelector = qualitySelector)
 
         assertThat(recorder.qualitySelector).isEqualTo(qualitySelector)
     }
 
     @Test
     fun canRetrieveProvidedExecutorFromRecorder() {
-        initializeRecorder()
         val myExecutor = Executor { command -> command?.run() }
-        val recorder = Recorder.Builder()
-            .setExecutor(myExecutor)
-            .build()
+        val recorder = createRecorder(executor = myExecutor)
 
         assertThat(recorder.executor).isSameInstanceAs(myExecutor)
     }
 
     @Test
     fun cannotRetrieveExecutorWhenExecutorNotProvided() {
-        initializeRecorder()
-        val recorder = Recorder.Builder().build()
+        val recorder = createRecorder()
 
         assertThat(recorder.executor).isNull()
     }
 
     @Test
     fun canRecordWithoutAudio() {
-        initializeRecorder()
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+        // Arrange.
+        val recording = createRecordingProcess(withAudio = false)
 
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        // Check the audio information reports state as disabled.
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
+        // Act.
+        recording.startAndVerify()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            val uri = finalize.outputResults.outputUri
+            checkFileHasAudioAndVideo(uri, hasAudio = false)
         }
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(VideoRecordEvent::class.java,
-            false, CallTimesAtLeast(1), captor)
-
-        assertThat(captor.value).isInstanceOf(VideoRecordEvent.Status::class.java)
-        val status = captor.value as VideoRecordEvent.Status
-        assertThat(status.recordingStats.audioStats.audioState)
-            .isEqualTo(AudioStats.AUDIO_STATE_DISABLED)
-
-        recording.stopSafely()
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(VideoRecordEvent.Finalize::class.java,
-            false, GENERAL_TIMEOUT)
-
-        checkFileAudio(Uri.fromFile(file), false)
-        checkFileVideo(Uri.fromFile(file), true)
-
-        file.delete()
     }
 
     @Test
     fun cannotStartMultiplePendingRecordingsWhileInitializing() {
-        initializeRecorder()
-        val file1 = File.createTempFile("CameraX1", ".tmp").apply { deleteOnExit() }
-        val file2 = File.createTempFile("CameraX2", ".tmp").apply { deleteOnExit() }
-        try {
-            // We explicitly do not invoke the surface request so the recorder is initializing.
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file1).build())
-                .start(CameraXExecutors.directExecutor()) {}
-                .apply {
-                    assertThrows<IllegalStateException> {
-                        recorder.prepareRecording(context, FileOutputOptions.Builder(file2).build())
-                            .start(CameraXExecutors.directExecutor()) {}
-                    }
-                    stopSafely()
-                }
-        } finally {
-            file1.delete()
-            file2.delete()
+        // Arrange: Prepare 1st recording and start.
+        val recorder = createRecorder(sendSurfaceRequest = false)
+        val recording = createRecordingProcess(recorder = recorder)
+        recording.start()
+
+        // Assert.
+        assertThrows<IllegalStateException> {
+            // Act: Prepare 2nd recording and start.
+            createRecordingProcess(recorder = recorder).start()
         }
     }
 
     @Test
     fun canRecoverFromErrorState(): Unit = runBlocking {
-        initializeRecorder()
+        // Arrange.
         // Create a video encoder factory that will fail on first 2 create encoder requests.
-        // Recorder initialization should fail by 1st encoder creation fail.
-        // 1st recording request should fail by 2nd encoder creation fail.
-        // 2nd recording request should be successful.
         var createEncoderRequestCount = 0
-        val recorder = Recorder.Builder()
-            .setVideoEncoderFactory { executor, config ->
-                if (createEncoderRequestCount < 2) {
-                    createEncoderRequestCount++
-                    throw InvalidConfigException("Create video encoder fail on purpose.")
-                } else {
-                    Recorder.DEFAULT_ENCODER_FACTORY.createEncoder(executor, config)
-                }
-            }.build().apply { onSourceStateChanged(VideoOutput.SourceState.INACTIVE) }
-
-        invokeSurfaceRequest(recorder)
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
+        val recorder = createRecorder(
+            videoEncoderFactory = { executor, config ->
+            if (createEncoderRequestCount < 2) {
+                createEncoderRequestCount++
+                throw InvalidConfigException("Create video encoder fail on purpose.")
+            } else {
+                Recorder.DEFAULT_ENCODER_FACTORY.createEncoder(executor, config)
+            }
+        })
+        // Recorder initialization should fail by 1st encoder creation fail.
         // Wait STREAM_ID_ERROR which indicates Recorder enter the error state.
         withTimeoutOrNull(3000) {
             recorder.streamInfo.asFlow().dropWhile { it!!.id != StreamInfo.STREAM_ID_ERROR }.first()
         } ?: fail("Do not observe STREAM_ID_ERROR from StreamInfo observer.")
 
-        // 1st recording request
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).let {
-                val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-                    VideoRecordEvent::class.java.isInstance(
-                        argument
-                    )
-                }
+        // Act: 1st recording request should fail by 2nd encoder creation fail.
+        var recording = createRecordingProcess(recorder = recorder)
+        recording.start()
+        recording.verifyFinalize { finalize ->
+            assertThat(finalize.error).isEqualTo(ERROR_RECORDER_ERROR)
+        }
 
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent::class.java,
-                    false,
-                    3000L,
-                    CallTimesAtLeast(1),
-                    captor
-                )
-
-                val finalize = captor.value as VideoRecordEvent.Finalize
-                assertThat(finalize.error).isEqualTo(ERROR_RECORDER_ERROR)
-            }
-
-        // 2nd recording request
-        mockVideoRecordEventConsumer.clearAcceptCalls()
-        recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).let {
-                mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-                it.stopSafely()
-
-                mockVideoRecordEventConsumer.verifyAcceptCall(
-                    VideoRecordEvent.Finalize::class.java,
-                    true, GENERAL_TIMEOUT
-                )
-            }
-
-        file.delete()
+        // Act: 2nd recording request should be successful.
+        recording = createRecordingProcess(recorder = recorder)
+        recording.startAndVerify()
+        recording.stopAndVerify()
     }
 
     @Test
     @SdkSuppress(minSdkVersion = 31)
     fun audioRecordIsAttributed() = runBlocking {
-        initializeRecorder()
+        // Arrange.
         val notedTag = CompletableDeferred<String>()
         val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager
-        appOps.setOnOpNotedCallback(
-            Dispatchers.Main.asExecutor(),
-            object : AppOpsManager.OnOpNotedCallback() {
-                override fun onNoted(p0: SyncNotedAppOp) {
-                    // no-op. record_audio should be async.
-                }
+        appOps.setOnOpNotedCallback(Dispatchers.Main.asExecutor(), object : OnOpNotedCallback() {
+            override fun onNoted(p0: SyncNotedAppOp) {
+                // no-op. record_audio should be async.
+            }
 
-                override fun onSelfNoted(p0: SyncNotedAppOp) {
-                    // no-op. record_audio should be async.
-                }
+            override fun onSelfNoted(p0: SyncNotedAppOp) {
+                // no-op. record_audio should be async.
+            }
 
-                override fun onAsyncNoted(noted: AsyncNotedAppOp) {
-                    if (AppOpsManager.OPSTR_RECORD_AUDIO == noted.op &&
-                        TEST_ATTRIBUTION_TAG == noted.attributionTag
-                    ) {
-                        notedTag.complete(noted.attributionTag!!)
-                    }
+            override fun onAsyncNoted(noted: AsyncNotedAppOp) {
+                if (AppOpsManager.OPSTR_RECORD_AUDIO == noted.op &&
+                    TEST_ATTRIBUTION_TAG == noted.attributionTag
+                ) {
+                    notedTag.complete(noted.attributionTag!!)
                 }
-            })
+            }
+        })
+        val attributionContext = context.createAttributionContext(TEST_ATTRIBUTION_TAG)
+        val recording = createRecordingProcess(context = attributionContext)
 
-        var recording: Recording? = null
+        // Act.
+        recording.start()
         try {
-            val attributionContext = context.createAttributionContext(TEST_ATTRIBUTION_TAG)
-            invokeSurfaceRequest()
-            val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
-            recording =
-                recorder.prepareRecording(
-                    attributionContext, FileOutputOptions.Builder(file).build()
-                )
-                    .withAudioEnabled()
-                    .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
             val timeoutDuration = 5.seconds
             withTimeoutOrNull(timeoutDuration) {
+                // Assert.
                 assertThat(notedTag.await()).isEqualTo(TEST_ATTRIBUTION_TAG)
             } ?: fail("Timed out waiting for attribution tag. Waited $timeoutDuration.")
         } finally {
             appOps.setOnOpNotedCallback(null, null)
-            recording?.stopSafely()
         }
     }
 
-    private fun invokeSurfaceRequest() {
-        invokeSurfaceRequest(recorder)
-    }
+    private fun testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(targetBitrate: Int) {
+        // Arrange.
+        val recorder = createRecorder(targetBitrate = targetBitrate)
+        val recording = createRecordingProcess(recorder = recorder, withAudio = false)
 
-    private fun invokeSurfaceRequest(recorder: Recorder) {
-        instrumentation.runOnMainSync {
-            preview.setSurfaceProvider { request: SurfaceRequest ->
-                recorder.onSurfaceRequested(request)
-            }
-            recorder.onSourceStateChanged(VideoOutput.SourceState.ACTIVE_STREAMING)
-        }
-    }
-
-    private fun initializeRecorder(bitrate: Int = BITRATE_AUTO) {
-        recorder = Recorder.Builder().apply {
-            if (bitrate != BITRATE_AUTO) {
-                setTargetVideoEncodingBitRate(bitrate)
-            }
-        }.build()
-        recorder.onSourceStateChanged(VideoOutput.SourceState.ACTIVE_NON_STREAMING)
-    }
-
-    private fun testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(
-        bitrate: Int,
-        enableAudio: Boolean = false
-    ) {
-        initializeRecorder(bitrate)
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-
-        val recording =
-            recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
-                .apply { if (enableAudio) withAudioEnabled() }
-                .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        recording.stopSafely()
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true,
-            GENERAL_TIMEOUT
-        )
-
-        val uri = Uri.fromFile(file)
-        if (enableAudio) {
-            checkFileHasAudioAndVideo(uri)
-        } else {
-            checkFileVideo(uri, true)
+        // Act.
+        recording.startAndVerify()
+        recording.stopAndVerify { finalize ->
+            assertThat(finalize.error).isEqualTo(ERROR_NONE)
         }
 
-        // Check the output Uri from the finalize event match the Uri from the given file.
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
-            VideoRecordEvent::class.java.isInstance(
-                argument
-            )
-        }
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent::class.java,
-            false,
-            CallTimesAtLeast(1),
-            captor
-        )
-
-        val finalize = captor.value as VideoRecordEvent.Finalize
-        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
-
-        file.delete()
-    }
-
-    private fun verifyConfiguredVideoBitrate() {
+        // Assert.
         assertThat(recorder.mFirstRecordingVideoBitrate).isIn(
             com.google.common.collect.Range.closed(
                 recorder.mVideoEncoderBitrateRange.lower,
@@ -1388,96 +919,224 @@
         )
     }
 
-    private fun checkFileHasAudioAndVideo(uri: Uri) {
-        checkFileAudio(uri, true)
-        checkFileVideo(uri, true)
-    }
-
-    private fun checkFileAudio(uri: Uri, hasAudio: Boolean) {
-        MediaMetadataRetriever().apply {
-            try {
-                setDataSource(context, uri)
-                val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
-
-                assertThat(value).isEqualTo(
-                    if (hasAudio) {
-                        "yes"
-                    } else {
-                        null
-                    }
-                )
-            } finally {
-                release()
+    private fun Recorder.sendSurfaceRequest() {
+        instrumentation.runOnMainSync {
+            preview.setSurfaceProvider { request: SurfaceRequest ->
+                onSurfaceRequested(request)
             }
         }
     }
 
-    private fun checkFileVideo(uri: Uri, hasVideo: Boolean) {
-        MediaMetadataRetriever().apply {
-            try {
-                setDataSource(context, uri)
-                val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
+    private fun createTempFile() = temporaryFolder.newFile()
 
-                assertThat(value).isEqualTo(
-                    if (hasVideo) {
-                        "yes"
-                    } else {
-                        null
-                    }
-                )
-            } finally {
-                release()
+    private fun createRecorder(
+        sendSurfaceRequest: Boolean = true,
+        initSourceState: VideoOutput.SourceState = ACTIVE_STREAMING,
+        qualitySelector: QualitySelector? = null,
+        executor: Executor? = null,
+        videoEncoderFactory: EncoderFactory? = null,
+        audioEncoderFactory: EncoderFactory? = null,
+        targetBitrate: Int? = null,
+    ): Recorder {
+        val recorder = Recorder.Builder().apply {
+            qualitySelector?.let { setQualitySelector(it) }
+            executor?.let { setExecutor(it) }
+            videoEncoderFactory?.let { setVideoEncoderFactory(it) }
+            audioEncoderFactory?.let { setAudioEncoderFactory(it) }
+            targetBitrate?.let { setTargetVideoEncodingBitRate(targetBitrate) }
+        }.build()
+        if (sendSurfaceRequest) {
+            recorder.sendSurfaceRequest()
+        }
+        recorder.onSourceStateChanged(initSourceState)
+        return recorder
+    }
+
+    private fun createFileOutputOptions(
+        file: File = createTempFile(),
+        fileSizeLimit: Long? = null,
+        durationLimitMillis: Long? = null,
+        location: Location? = null,
+    ): FileOutputOptions = FileOutputOptions.Builder(file).apply {
+        fileSizeLimit?.let { setFileSizeLimit(it) }
+        durationLimitMillis?.let { setDurationLimitMillis(it) }
+        location?.let { setLocation(it) }
+    }.build()
+
+    private fun createRecordingProcess(
+        recorder: Recorder = createRecorder(),
+        context: Context = ApplicationProvider.getApplicationContext(),
+        outputOptions: OutputOptions = createFileOutputOptions(),
+        withAudio: Boolean = true
+    ) = RecordingProcess(
+        recorder,
+        context,
+        outputOptions,
+        withAudio
+    )
+
+    inner class RecordingProcess(
+        private val recorder: Recorder,
+        context: Context,
+        outputOptions: OutputOptions,
+        withAudio: Boolean
+    ) {
+        private val pendingRecording: PendingRecording =
+            PendingRecording(context, recorder, outputOptions).apply {
+                if (withAudio) {
+                    withAudioEnabled()
+                }
             }
+        val listener = MockConsumer<VideoRecordEvent>()
+        private lateinit var recording: Recording
+
+        fun startAndVerify(
+            statusCount: Int = DEFAULT_STATUS_COUNT,
+            onStatus: ((List<Status>) -> Unit)? = null,
+        ) = startInternal(verify = true, statusCount = statusCount, onStatus = onStatus)
+
+        fun start() = startInternal(verify = false)
+
+        private fun startInternal(
+            verify: Boolean = false,
+            statusCount: Int = DEFAULT_STATUS_COUNT,
+            onStatus: ((List<Status>) -> Unit)? = null
+        ) {
+            recording = pendingRecording.start(mainThreadExecutor(), listener)
+            recordingsToStop.add(this)
+            if (verify) {
+                verifyStart()
+                verifyStatus(statusCount = statusCount, onStatus = onStatus)
+            }
+        }
+
+        fun verifyStart() {
+            listener.verifyStart()
+        }
+
+        fun verifyStatus(
+            statusCount: Int = DEFAULT_STATUS_COUNT,
+            onStatus: ((List<Status>) -> Unit)? = null,
+        ) {
+            listener.verifyStatus(eventCount = statusCount, onEvent = onStatus)
+        }
+
+        fun stopAndVerify(onFinalize: ((Finalize) -> Unit)? = null) =
+            stopInternal(verify = true, onFinalize)
+
+        fun stop() = stopInternal(verify = false)
+
+        private fun stopInternal(
+            verify: Boolean = false,
+            onFinalize: ((Finalize) -> Unit)? = null
+        ) {
+            recording.stopSafely(recorder)
+            if (verify) {
+                verifyFinalize(onFinalize = onFinalize)
+            }
+        }
+
+        fun verifyFinalize(
+            timeoutMs: Long = GENERAL_TIMEOUT,
+            onFinalize: ((Finalize) -> Unit)? = null
+        ) = listener.verifyFinalize(timeoutMs = timeoutMs, onFinalize = onFinalize)
+
+        fun pauseAndVerify() = pauseInternal(verify = true)
+
+        fun pause() = pauseInternal(verify = false)
+
+        private fun pauseInternal(verify: Boolean = false) {
+            recording.pause()
+            if (verify) {
+                verifyPause()
+            }
+        }
+
+        fun verifyPause() = listener.verifyPause()
+
+        fun resumeAndVerify() = resumeInternal(verify = true)
+
+        fun resume() = resumeInternal(verify = false)
+
+        private fun resumeInternal(verify: Boolean = false) {
+            recording.resume()
+            if (verify) {
+                verifyResume()
+            }
+        }
+
+        private fun verifyResume() {
+            listener.verifyResume()
+            listener.verifyStatus()
+        }
+
+        fun getAllEvents(): List<VideoRecordEvent> {
+            lateinit var events: List<VideoRecordEvent>
+            listener.verifyEvent(
+                VideoRecordEvent::class.java,
+                CallTimesAtLeast(1),
+                onEvent = {
+                    events = it
+                }
+            )
+            return events
+        }
+
+        fun verifyNoMoreEvent() = listener.verifyNoMoreAcceptCalls(/*inOrder=*/true)
+    }
+
+    private fun checkFileHasAudioAndVideo(
+        uri: Uri,
+        hasAudio: Boolean = true,
+    ) {
+        MediaMetadataRetriever().useAndRelease {
+            it.setDataSource(context, uri)
+            assertThat(it.hasVideo()).isEqualTo(true)
+            assertThat(it.hasAudio()).isEqualTo(hasAudio)
         }
     }
 
+    @Suppress("SameParameterValue")
     private fun checkLocation(uri: Uri, location: Location) {
-        MediaMetadataRetriever().apply {
-            try {
-                setDataSource(context, uri)
-                // Only test on mp4 output format, others will be ignored.
-                val mime = extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
-                assumeTrue("Unsupported mime = $mime",
-                    "video/mp4".equals(mime, ignoreCase = true))
-                val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
-                assertThat(value).isNotNull()
-                // ex: (90, 180) => "+90.0000+180.0000/" (ISO-6709 standard)
-                val matchGroup =
-                    "([\\+-]?[0-9]+(\\.[0-9]+)?)([\\+-]?[0-9]+(\\.[0-9]+)?)".toRegex()
-                        .find(value!!) ?: fail("Fail on checking location metadata: $value")
-                val lat = matchGroup.groupValues[1].toDouble()
-                val lon = matchGroup.groupValues[3].toDouble()
+        MediaMetadataRetriever().useAndRelease {
+            it.setDataSource(context, uri)
+            // Only test on mp4 output format, others will be ignored.
+            val mime = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
+            assumeTrue("Unsupported mime = $mime",
+                "video/mp4".equals(mime, ignoreCase = true))
+            val value = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
+            assertThat(value).isNotNull()
+            // ex: (90, 180) => "+90.0000+180.0000/" (ISO-6709 standard)
+            val matchGroup =
+                "([+-]?[0-9]+(\\.[0-9]+)?)([+-]?[0-9]+(\\.[0-9]+)?)".toRegex()
+                    .find(value!!) ?: fail("Fail on checking location metadata: $value")
+            val lat = matchGroup.groupValues[1].toDouble()
+            val lon = matchGroup.groupValues[3].toDouble()
 
-                // MediaMuxer.setLocation rounds the value to 4 decimal places
-                val tolerance = 0.0001
-                assertWithMessage("Fail on latitude. $lat($value) vs ${location.latitude}")
-                    .that(lat).isWithin(tolerance).of(location.latitude)
-                assertWithMessage("Fail on longitude. $lon($value) vs ${location.longitude}")
-                    .that(lon).isWithin(tolerance).of(location.longitude)
-            } finally {
-                release()
-            }
+            // MediaMuxer.setLocation rounds the value to 4 decimal places
+            val tolerance = 0.0001
+            assertWithMessage("Fail on latitude. $lat($value) vs ${location.latitude}")
+                .that(lat).isWithin(tolerance).of(location.latitude)
+            assertWithMessage("Fail on longitude. $lon($value) vs ${location.longitude}")
+                .that(lon).isWithin(tolerance).of(location.longitude)
         }
     }
 
+    @Suppress("SameParameterValue")
     private fun checkDurationAtMost(uri: Uri, duration: Long) {
-        MediaMetadataRetriever().apply {
-            try {
-                setDataSource(context, uri)
-                val durationFromFile = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
+        MediaMetadataRetriever().useAndRelease {
+            it.setDataSource(context, uri)
+            val durationFromFile = it.getDuration()
 
-                assertThat(durationFromFile).isNotNull()
-                assertThat(durationFromFile!!.toLong()).isAtMost(duration)
-            } finally {
-                release()
-            }
+            assertThat(durationFromFile).isNotNull()
+            assertThat(durationFromFile!!).isAtMost(duration)
         }
     }
 
     // It fails on devices with certain chipset if the codec is stopped when the camera is still
     // producing frames to the provided surface. This method first stop the camera from
     // producing frames then stops the recording safely on the problematic devices.
-    private fun Recording.stopSafely() {
+    private fun Recording.stopSafely(recorder: Recorder) {
         val deactivateSurfaceBeforeStop =
             DeviceQuirks.get(DeactivateEncoderSurfaceBeforeStopEncoderQuirk::class.java) != null
         if (deactivateSurfaceBeforeStop) {
@@ -1487,70 +1146,21 @@
         }
         stop()
         if (deactivateSurfaceBeforeStop && Build.VERSION.SDK_INT >= 23) {
-            invokeSurfaceRequest()
+            recorder.sendSurfaceRequest()
         }
     }
 
-    private fun runFileSizeLimitTest(fileSizeLimit: Long) {
-        // For the file size is small, the final file length possibly exceeds the file size limit
-        // after adding the file header. We still add the buffer for the tolerance of comparing the
-        // file length and file size limit.
-        val sizeLimitBuffer = 50 * 1024 // 50k threshold buffer
-        invokeSurfaceRequest()
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-        val outputOptions = FileOutputOptions.Builder(file)
-            .setFileSizeLimit(fileSizeLimit)
-            .build()
-
-        val recording = recorder
-            .prepareRecording(context, outputOptions)
-            .withAudioEnabled()
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            false, 60000L
-        )
-
-        val captor = ArgumentCaptorCameraX<VideoRecordEvent> {
-                argument -> VideoRecordEvent::class.java.isInstance(argument)
-        }
-        mockVideoRecordEventConsumer.verifyAcceptCall(VideoRecordEvent::class.java,
-            false, CallTimesAtLeast(1), captor)
-
-        assertThat(captor.value).isInstanceOf(VideoRecordEvent.Finalize::class.java)
-        val finalize = captor.value as VideoRecordEvent.Finalize
-        assertThat(finalize.error).isEqualTo(ERROR_FILE_SIZE_LIMIT_REACHED)
-        assertThat(file.length()).isLessThan(fileSizeLimit + sizeLimitBuffer)
-
-        recording.stopSafely()
-
-        file.delete()
-    }
-
     private fun runLocationTest(location: Location) {
-        invokeSurfaceRequest(recorder)
-        val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
-        val outputOptions = FileOutputOptions.Builder(file)
-            .setLocation(location)
-            .build()
+        // Arrange.
+        val outputOptions = createFileOutputOptions(location = location)
+        val recording = createRecordingProcess(outputOptions = outputOptions)
 
-        val recording = recorder
-            .prepareRecording(context, outputOptions)
-            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer)
-
-        mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
-
-        recording.stopSafely()
-
-        mockVideoRecordEventConsumer.verifyAcceptCall(
-            VideoRecordEvent.Finalize::class.java,
-            true, GENERAL_TIMEOUT
-        )
-
-        checkLocation(Uri.fromFile(file), location)
-
-        file.delete()
+        // Act.
+        recording.startAndVerify()
+        recording.stopAndVerify { finalize ->
+            // Assert.
+            checkLocation(finalize.outputResults.outputUri, location)
+        }
     }
 
     private fun createLocation(
@@ -1563,50 +1173,85 @@
             this.longitude = longitude
         }
 
-    private fun MockConsumer<VideoRecordEvent>.verifyRecordingStartSuccessfully() {
-        verifyAcceptCall(
-            VideoRecordEvent.Start::class.java,
-            true,
-            GENERAL_TIMEOUT
-        )
-        verifyAcceptCall(
-            VideoRecordEvent.Status::class.java,
-            true,
-            STATUS_TIMEOUT,
-            CallTimesAtLeast(5)
+    private fun MockConsumer<VideoRecordEvent>.verifyStart(
+        inOrder: Boolean = true,
+        onEvent: ((Start) -> Unit)? = null
+    ) {
+        verifyEvent(Start::class.java, inOrder = inOrder, onEvent = onEvent)
+    }
+
+    private fun MockConsumer<VideoRecordEvent>.verifyFinalize(
+        inOrder: Boolean = true,
+        timeoutMs: Long = GENERAL_TIMEOUT,
+        onFinalize: ((Finalize) -> Unit)? = null
+    ) {
+        verifyEvent(
+            Finalize::class.java,
+            inOrder = inOrder,
+            timeoutMs = timeoutMs,
+            onEvent = onFinalize
         )
     }
-    class VideoCaptureMonitor : Consumer<VideoRecordEvent> {
-        private var countDown: CountDownLatch? = null
 
-        fun waitForVideoCaptureStatus(
-            count: Int = 10,
-            timeoutMillis: Long = TimeUnit.SECONDS.toMillis(10)
-        ) {
-            assertWithMessage("Video recording doesn't start").that(synchronized(this) {
-                countDown = CountDownLatch(count)
-                countDown
-            }!!.await(timeoutMillis, TimeUnit.MILLISECONDS)).isTrue()
+    private fun MockConsumer<VideoRecordEvent>.verifyStatus(
+        eventCount: Int = DEFAULT_STATUS_COUNT,
+        inOrder: Boolean = true,
+        onEvent: ((List<Status>) -> Unit)? = null,
+    ) {
+        verifyEvent(
+            Status::class.java,
+            CallTimesAtLeast(eventCount),
+            inOrder = inOrder,
+            timeoutMs = STATUS_TIMEOUT,
+            onEvent = onEvent
+        )
+    }
+
+    private fun MockConsumer<VideoRecordEvent>.verifyPause(
+        inOrder: Boolean = true,
+        onEvent: ((Pause) -> Unit)? = null
+    ) {
+        verifyEvent(Pause::class.java, inOrder = inOrder, onEvent = onEvent)
+    }
+
+    private fun MockConsumer<VideoRecordEvent>.verifyResume(
+        inOrder: Boolean = true,
+        onEvent: ((Resume) -> Unit)? = null,
+    ) {
+        verifyEvent(Resume::class.java, inOrder = inOrder, onEvent = onEvent)
+    }
+
+    private fun <T : VideoRecordEvent> MockConsumer<VideoRecordEvent>.verifyEvent(
+        eventType: Class<T>,
+        inOrder: Boolean = false,
+        timeoutMs: Long = GENERAL_TIMEOUT,
+        onEvent: ((T) -> Unit)? = null,
+    ) {
+        verifyEvent(
+            eventType,
+            callTimes = CallTimes(1),
+            inOrder = inOrder,
+            timeoutMs = timeoutMs
+        ) { events ->
+            onEvent?.invoke(events.last())
         }
+    }
 
-        override fun accept(event: VideoRecordEvent?) {
-            when (event) {
-                is VideoRecordEvent.Status -> {
-                    synchronized(this) {
-                        countDown?.countDown()
-                    }
-                }
-                else -> {
-                    // Ignore other events.
-                }
+    private fun <T : VideoRecordEvent> MockConsumer<VideoRecordEvent>.verifyEvent(
+        eventType: Class<T>,
+        callTimes: CallTimes,
+        inOrder: Boolean = false,
+        timeoutMs: Long = GENERAL_TIMEOUT,
+        onEvent: ((List<T>) -> Unit)? = null,
+    ) {
+        verifyAcceptCall(eventType, inOrder, timeoutMs, callTimes)
+        if (onEvent != null) {
+            val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
+                eventType.isInstance(argument)
             }
+            verifyAcceptCall(eventType, false, callTimes, captor)
+            @Suppress("UNCHECKED_CAST")
+            onEvent.invoke(captor.allValues as List<T>)
         }
     }
-
-    private fun Recorder.startVideoRecording(
-        file: File,
-        eventListener: Consumer<VideoRecordEvent>
-    ): Recording = prepareRecording(
-        context, FileOutputOptions.Builder(file).build()
-    ).start(Dispatchers.Main.asExecutor(), eventListener)
 }
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/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index 3b00ca9..912c57a 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -448,7 +448,7 @@
         instrumentation.runOnMainSync {
             cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, videoCapture)
         }
-        val videoCaptureMonitor = RecorderTest.VideoCaptureMonitor()
+        val videoCaptureMonitor = VideoCaptureMonitor()
         videoCapture.startVideoRecording(temporaryFolder.newFile(), videoCaptureMonitor).use {
             // Ensure the Recorder is initialized before start test.
             videoCaptureMonitor.waitForVideoCaptureStatus()
@@ -1080,10 +1080,46 @@
         )
 }
 
-private fun MediaMetadataRetriever.useAndRelease(block: (MediaMetadataRetriever) -> Unit) {
+private class VideoCaptureMonitor : Consumer<VideoRecordEvent> {
+    private var countDown: CountDownLatch? = null
+
+    fun waitForVideoCaptureStatus(
+        count: Int = 10,
+        timeoutMillis: Long = TimeUnit.SECONDS.toMillis(10)
+    ) {
+        assertWithMessage("Video recording doesn't start").that(synchronized(this) {
+            countDown = CountDownLatch(count)
+            countDown
+        }!!.await(timeoutMillis, TimeUnit.MILLISECONDS)).isTrue()
+    }
+
+    override fun accept(event: VideoRecordEvent?) {
+        when (event) {
+            is VideoRecordEvent.Status -> {
+                synchronized(this) {
+                    countDown?.countDown()
+                }
+            }
+            else -> {
+                // Ignore other events.
+            }
+        }
+    }
+}
+
+internal fun MediaMetadataRetriever.useAndRelease(block: (MediaMetadataRetriever) -> Unit) {
     try {
         block(this)
     } finally {
         release()
     }
 }
+
+internal fun MediaMetadataRetriever.hasAudio(): Boolean =
+    extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO) == "yes"
+
+internal fun MediaMetadataRetriever.hasVideo(): Boolean =
+    extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO) == "yes"
+
+internal fun MediaMetadataRetriever.getDuration(): Long? =
+    extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()
\ 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 5f5ee52..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
@@ -433,8 +433,12 @@
     }
 
     @Test
-    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
+    @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-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index 296069a8..2ed53fc 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -309,6 +309,15 @@
             TAP_TO_FOCUS_NOT_STARTED);
 
     @NonNull
+    private final PendingValue<Boolean> mPendingEnableTorch = new PendingValue<>();
+
+    @NonNull
+    private final PendingValue<Float> mPendingLinearZoom = new PendingValue<>();
+
+    @NonNull
+    private final PendingValue<Float> mPendingZoomRatio = new PendingValue<>();
+
+    @NonNull
     private final Set<CameraEffect> mEffects = new HashSet<>();
 
     private final Context mAppContext;
@@ -1806,8 +1815,7 @@
     public ListenableFuture<Void> setZoomRatio(float zoomRatio) {
         checkMainThread();
         if (!isCameraAttached()) {
-            Logger.w(TAG, CAMERA_NOT_ATTACHED);
-            return Futures.immediateFuture(null);
+            return mPendingZoomRatio.setValue(zoomRatio);
         }
         return mCamera.getCameraControl().setZoomRatio(zoomRatio);
     }
@@ -1834,8 +1842,7 @@
     public ListenableFuture<Void> setLinearZoom(@FloatRange(from = 0f, to = 1f) float linearZoom) {
         checkMainThread();
         if (!isCameraAttached()) {
-            Logger.w(TAG, CAMERA_NOT_ATTACHED);
-            return Futures.immediateFuture(null);
+            return mPendingLinearZoom.setValue(linearZoom);
         }
         return mCamera.getCameraControl().setLinearZoom(linearZoom);
     }
@@ -1873,8 +1880,7 @@
     public ListenableFuture<Void> enableTorch(boolean torchEnabled) {
         checkMainThread();
         if (!isCameraAttached()) {
-            Logger.w(TAG, CAMERA_NOT_ATTACHED);
-            return Futures.immediateFuture(null);
+            return mPendingEnableTorch.setValue(torchEnabled);
         }
         return mCamera.getCameraControl().enableTorch(torchEnabled);
     }
@@ -1955,6 +1961,9 @@
         }
         mZoomState.setSource(mCamera.getCameraInfo().getZoomState());
         mTorchState.setSource(mCamera.getCameraInfo().getTorchState());
+        mPendingEnableTorch.propagateIfHasValue(this::enableTorch);
+        mPendingLinearZoom.propagateIfHasValue(this::setLinearZoom);
+        mPendingZoomRatio.propagateIfHasValue(this::setZoomRatio);
     }
 
     /**
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PendingValue.java b/camera/camera-view/src/main/java/androidx/camera/view/PendingValue.java
new file mode 100644
index 0000000..5b5d1fd
--- /dev/null
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PendingValue.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.view;
+
+import static androidx.camera.core.impl.utils.Threads.checkMainThread;
+import static androidx.core.util.Preconditions.checkState;
+
+import static java.util.Objects.requireNonNull;
+
+import android.os.Build;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.arch.core.util.Function;
+import androidx.camera.core.CameraControl;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * Pending value assignment that wait for event like camera initialization.
+ *
+ * <p> For example, one cannot call {@link CameraControl#setZoomRatio} before camera initialization
+ * completes. To work around this, in {@link CameraController} when app calls
+ * {@link CameraController#setZoomRatio}, we will cache the value with this class and propagate it
+ * when {@link CameraControl} becomes ready.
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+class PendingValue<T> {
+
+    @Nullable
+    private T mValue;
+    @Nullable
+    private ListenableFuture<Void> mListenableFuture;
+    @Nullable
+    private CallbackToFutureAdapter.Completer<Void> mCompleter;
+
+    /**
+     * Assigns the pending value.
+     *
+     * @return a {@link ListenableFuture} that completes when the assignment completes.
+     */
+    @MainThread
+    ListenableFuture<Void> setValue(@NonNull T value) {
+        checkMainThread();
+        if (mListenableFuture != null) {
+            checkState(!mListenableFuture.isDone(),
+                    "#setValue() is called after the value is propagated.");
+            // Cancel the previous ListenableFuture.
+            mListenableFuture.cancel(false);
+        }
+        // Track the pending value and the ListenableFuture.
+        mValue = value;
+        mListenableFuture = CallbackToFutureAdapter.getFuture(completer -> {
+            mCompleter = completer;
+            return "PendingValue " + value;
+        });
+        return mListenableFuture;
+    }
+
+    /**
+     * Propagates the value if a pending value exists.
+     *
+     * <p> This method no-ops if there is no pending value.
+     */
+    @MainThread
+    void propagateIfHasValue(Function<T, ListenableFuture<Void>> setValueFunction) {
+        checkMainThread();
+        if (mValue != null) {
+            Futures.propagate(setValueFunction.apply(mValue), requireNonNull(mCompleter));
+        }
+    }
+}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt b/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
index cc60468..48f5899 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
+++ b/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
@@ -36,6 +36,7 @@
 import androidx.camera.core.impl.ImageOutputConfig
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraControl
 import androidx.camera.testing.fakes.FakeLifecycleOwner
 import androidx.camera.video.Quality
 import androidx.camera.view.CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED
@@ -60,6 +61,11 @@
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class CameraControllerTest {
+    companion object {
+        const val LINEAR_ZOOM = .1F
+        const val ZOOM_RATIO = .5F
+        const val TORCH_ENABLED = true
+    }
 
     private val context = ApplicationProvider.getApplicationContext<Context>()
     private lateinit var controller: LifecycleCameraController
@@ -69,7 +75,8 @@
         CameraController.OutputSize(Size(1080, 1960))
     private val targetVideoQuality = Quality.HIGHEST
     private val fakeViewPort = ViewPort.Builder(Rational(1, 1), 0).build()
-    private val fakeCamera = FakeCamera()
+    private val fakeCameraControl = FakeCameraControl()
+    private val fakeCamera = FakeCamera(fakeCameraControl)
     private val processCameraProviderWrapper = FakeProcessCameraProviderWrapper(fakeCamera)
     private lateinit var lifecycleCameraProviderCompleter:
         CallbackToFutureAdapter.Completer<ProcessCameraProviderWrapper>
@@ -86,6 +93,31 @@
     }
 
     @Test
+    fun setPendingValues_valuesPropagateAfterInit() {
+        // Arrange: set pending values
+        val linearZoomFuture = controller.setLinearZoom(LINEAR_ZOOM)
+        val zoomRatioFuture = controller.setZoomRatio(ZOOM_RATIO)
+        val torchFuture = controller.enableTorch(TORCH_ENABLED)
+        assertThat(fakeCameraControl.linearZoom).isNotEqualTo(LINEAR_ZOOM)
+        assertThat(fakeCameraControl.zoomRatio).isNotEqualTo(ZOOM_RATIO)
+        assertThat(fakeCameraControl.torchEnabled).isNotEqualTo(TORCH_ENABLED)
+        assertThat(linearZoomFuture.isDone).isFalse()
+        assertThat(zoomRatioFuture.isDone).isFalse()
+        assertThat(torchFuture.isDone).isFalse()
+
+        // Act.
+        completeCameraInitialization()
+
+        // Assert:
+        assertThat(fakeCameraControl.linearZoom).isEqualTo(LINEAR_ZOOM)
+        assertThat(fakeCameraControl.zoomRatio).isEqualTo(ZOOM_RATIO)
+        assertThat(fakeCameraControl.torchEnabled).isEqualTo(TORCH_ENABLED)
+        assertThat(linearZoomFuture.isDone).isTrue()
+        assertThat(zoomRatioFuture.isDone).isTrue()
+        assertThat(torchFuture.isDone).isTrue()
+    }
+
+    @Test
     fun initCompletes_torchStatePropagated() {
         // Arrange: get LiveData before init completes
         val torchState = controller.torchState
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/FakeProcessCameraProviderWrapper.kt b/camera/camera-view/src/test/java/androidx/camera/view/FakeProcessCameraProviderWrapper.kt
index a027c4f..9ad8fd3 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/FakeProcessCameraProviderWrapper.kt
+++ b/camera/camera-view/src/test/java/androidx/camera/view/FakeProcessCameraProviderWrapper.kt
@@ -21,13 +21,20 @@
 import androidx.camera.core.UseCase
 import androidx.camera.core.UseCaseGroup
 import androidx.camera.core.impl.utils.futures.Futures
+import androidx.camera.testing.fakes.FakeCamera
 import androidx.lifecycle.LifecycleOwner
 import com.google.common.util.concurrent.ListenableFuture
 
 /**
  * Fake [ProcessCameraProviderWrapper].
+ *
+ * @param bindToLifecycleException the [Exception] to throw when [bindToLifecycle] is called.
+ * If null, [bindToLifecycle] will not throw any error.
  */
-class FakeProcessCameraProviderWrapper(private val camera: Camera) : ProcessCameraProviderWrapper {
+class FakeProcessCameraProviderWrapper(
+    private val camera: Camera = FakeCamera(),
+    private val bindToLifecycleException: Throwable? = null
+) : ProcessCameraProviderWrapper {
 
     override fun hasCamera(cameraSelector: CameraSelector): Boolean {
         return true
@@ -46,6 +53,9 @@
         cameraSelector: CameraSelector,
         useCaseGroup: UseCaseGroup
     ): Camera {
+        if (bindToLifecycleException != null) {
+            throw bindToLifecycleException
+        }
         return camera
     }
 
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PendingValueTest.kt b/camera/camera-view/src/test/java/androidx/camera/view/PendingValueTest.kt
new file mode 100644
index 0000000..06d7972
--- /dev/null
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PendingValueTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view
+
+import android.os.Build
+import androidx.camera.core.impl.utils.futures.Futures
+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 [PendingValue].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class PendingValueTest {
+
+    @Test
+    fun assignPendingValueTwice_theSecondValueIsAssigned() {
+        // Arrange.
+        val pendingValue = PendingValue<Boolean>()
+
+        // Act: set value twice: false then true.
+        val future1 = pendingValue.setValue(false)
+        val future2 = pendingValue.setValue(true)
+
+        // Assert: the value is true.
+        var assignedValue: Boolean? = null
+        pendingValue.propagateIfHasValue {
+            assignedValue = it
+            Futures.immediateFuture(null)
+        }
+        assertThat(assignedValue).isTrue()
+        assertThat(future1.isCancelled).isTrue()
+        assertThat(future2.isCancelled).isFalse()
+        assertThat(future2.isDone).isTrue()
+    }
+}
\ No newline at end of file
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/FocusMeteringDeviceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
index b7c5042..a428460 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
@@ -51,7 +51,6 @@
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import org.hamcrest.CoreMatchers.equalTo
-import org.hamcrest.CoreMatchers.not
 import org.junit.After
 import org.junit.Assume
 import org.junit.Assume.assumeThat
@@ -262,11 +261,6 @@
 
     @Test
     fun focusMeteringFailsWithIllegalArgumentException_whenMeteringPointInvalid() = runBlocking {
-        assumeThat(
-            "Test ignored for CameraPipe config: b/263323720",
-            implName, not(CameraPipeConfig::class.simpleName)
-        )
-
         val focusMeteringAction = FocusMeteringAction.Builder(invalidMeteringPoint).build()
 
         assumeThat(
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/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCaptureSessionStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCaptureSessionStressTest.kt
index 0edd0a0..925d785 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCaptureSessionStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/OpenCloseCaptureSessionStressTest.kt
@@ -17,27 +17,17 @@
 package androidx.camera.integration.extensions
 
 import android.content.Context
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraDevice
+import android.view.Surface
 import androidx.camera.camera2.Camera2Config
-import androidx.camera.camera2.impl.Camera2ImplConfig
-import androidx.camera.camera2.impl.CameraEventCallback
-import androidx.camera.camera2.impl.CameraEventCallbacks
-import androidx.camera.camera2.interop.Camera2CameraInfo
+import androidx.camera.camera2.interop.Camera2Interop
 import androidx.camera.core.Camera
-import androidx.camera.core.CameraFilter
-import androidx.camera.core.CameraInfo
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
 import androidx.camera.core.UseCase
-import androidx.camera.core.impl.CameraConfig
-import androidx.camera.core.impl.CaptureConfig
-import androidx.camera.core.impl.Config
-import androidx.camera.core.impl.ExtendedCameraConfigProviderStore
-import androidx.camera.core.impl.Identifier
-import androidx.camera.core.impl.MutableOptionsBundle
-import androidx.camera.core.impl.OptionsBundle
-import androidx.camera.core.impl.UseCaseConfigFactory
 import androidx.camera.extensions.ExtensionMode
 import androidx.camera.extensions.ExtensionsManager
 import androidx.camera.integration.extensions.util.CameraXExtensionsTestUtil
@@ -88,7 +78,7 @@
     private lateinit var imageAnalysis: ImageAnalysis
     private var isImageAnalysisSupported = false
     private lateinit var lifecycleOwner: FakeLifecycleOwner
-    private val cameraEventMonitor = CameraEventMonitor()
+    private val cameraSessionMonitor = CameraSessionMonitor()
 
     @Before
     fun setUp(): Unit = runBlocking {
@@ -114,7 +104,9 @@
             cameraProvider.bindToLifecycle(lifecycleOwner, extensionCameraSelector)
         }
 
-        preview = Preview.Builder().build()
+        val previewBuilder = Preview.Builder()
+        injectCameraSessionMonitor(previewBuilder, cameraSessionMonitor)
+        preview = previewBuilder.build()
         withContext(Dispatchers.Main) {
             preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
         }
@@ -125,6 +117,51 @@
             camera.isUseCasesCombinationSupported(preview, imageCapture, imageAnalysis)
     }
 
+    private fun injectCameraSessionMonitor(
+        previewBuilder: Preview.Builder,
+        cameraMonitor: CameraSessionMonitor
+    ) {
+        Camera2Interop.Extender(previewBuilder)
+            .setSessionStateCallback(object : CameraCaptureSession.StateCallback() {
+                override fun onConfigured(session: CameraCaptureSession) {
+                    cameraMonitor.onOpenedSession()
+                }
+
+                override fun onClosed(session: CameraCaptureSession) {
+                    cameraMonitor.onClosedSession()
+                }
+
+                override fun onConfigureFailed(session: CameraCaptureSession) {
+                }
+
+                override fun onReady(session: CameraCaptureSession) {
+                }
+
+                override fun onActive(session: CameraCaptureSession) {
+                }
+
+                override fun onCaptureQueueEmpty(session: CameraCaptureSession) {
+                }
+
+                override fun onSurfacePrepared(session: CameraCaptureSession, surface: Surface) {
+                }
+            })
+            .setDeviceStateCallback(object : CameraDevice.StateCallback() {
+                override fun onOpened(device: CameraDevice) {
+                }
+                // Some device doesn't invoke CameraCaptureSession onClosed callback thus
+                // we need to invoke when camera is closed.
+                override fun onClosed(device: CameraDevice) {
+                    cameraMonitor.onClosedSession()
+                }
+
+                override fun onDisconnected(device: CameraDevice) {
+                }
+
+                override fun onError(device: CameraDevice, error: Int) {
+                }
+            })
+    }
     @After
     fun cleanUp(): Unit = runBlocking {
         if (::cameraProvider.isInitialized) {
@@ -157,7 +194,7 @@
 
     /**
      * Repeatedly binds use cases, unbind all to check whether the capture session can be opened
-     * and closed successfully by monitoring the CameraEvent callbacks.
+     * and closed successfully by monitoring the camera session state.
      */
     private fun bindUseCase_unbindAll_toCheckCameraEvent_repeatedly(
         vararg useCases: UseCase,
@@ -165,36 +202,27 @@
     ): Unit = runBlocking {
         for (i in 1..repeatCount) {
             // Arrange: resets the camera event monitor
-            cameraEventMonitor.reset()
+            cameraSessionMonitor.reset()
 
             withContext(Dispatchers.Main) {
-                // Arrange: retrieves the camera selector which allows to monitor camera event
-                // callbacks
-                val extensionEnabledCameraEventMonitorCameraSelector =
-                    getExtensionsCameraEventMonitorCameraSelector(
-                        extensionsManager,
-                        config.extensionMode,
-                        baseCameraSelector
-                    )
-
                 // Act: binds use cases
                 cameraProvider.bindToLifecycle(
                     lifecycleOwner,
-                    extensionEnabledCameraEventMonitorCameraSelector,
+                    extensionCameraSelector,
                     *useCases
                 )
             }
 
-            // Assert: checks the CameraEvent#onEnableSession callback function is called
-            cameraEventMonitor.awaitSessionEnabledAndAssert()
+            // Assert: checks the camera session is opened.
+            cameraSessionMonitor.awaitSessionOpenedAndAssert()
 
             // Act: unbinds all use cases
             withContext(Dispatchers.Main) {
                 cameraProvider.unbindAll()
             }
 
-            // Assert: checks the CameraEvent#onSessionDisabled callback function is called
-            cameraEventMonitor.awaitSessionDisabledAndAssert()
+            // Assert: checks the camera session is closed.
+            cameraSessionMonitor.awaitSessionClosedAndAssert()
         }
     }
 
@@ -231,186 +259,18 @@
     }
 
     /**
-     * Gets the camera selector which allows to monitor the camera event callbacks
+     * An implementation of CameraEventCallback to monitor whether the camera is closed or opened.
      */
-    private fun getExtensionsCameraEventMonitorCameraSelector(
-        extensionsManager: ExtensionsManager,
-        extensionMode: Int,
-        baseCameraSelector: CameraSelector
-    ): CameraSelector {
-        // Injects the ExtensionsCameraEventMonitorUseCaseConfigFactory which allows to monitor and
-        // verify the camera event callbacks
-        injectExtensionsCameraEventMonitorUseCaseConfigFactory(
-            extensionsManager,
-            extensionMode,
-            baseCameraSelector
-        )
-
-        val builder = CameraSelector.Builder.fromSelector(baseCameraSelector)
-        // Add an ExtensionCameraEventMonitorCameraFilter which includes the CameraFilter to check
-        // whether the camera is supported for the extension mode or not and also includes the
-        // identifier to find the extended camera config provider from
-        // ExtendedCameraConfigProviderStore
-        builder.addCameraFilter(
-            ExtensionsCameraEventMonitorCameraFilter(
-                extensionsManager,
-                extensionMode
-            )
-        )
-        return builder.build()
-    }
-
-    /**
-     * Injects the ExtensionsCameraEventMonitorUseCaseConfigFactory which allows to monitor and
-     * verify the camera event callbacks
-     */
-    private fun injectExtensionsCameraEventMonitorUseCaseConfigFactory(
-        extensionsManager: ExtensionsManager,
-        extensionMode: Int,
-        baseCameraSelector: CameraSelector
-    ): Unit = runBlocking {
-        val defaultConfigProviderId =
-            Identifier.create(getExtendedCameraConfigProviderId(extensionMode))
-        val cameraEventConfigProviderId =
-            Identifier.create(getCameraEventMonitorCameraConfigProviderId(extensionMode))
-
-        // Calls the ExtensionsManager#getExtensionEnabledCameraSelector() function to add the
-        // default extended camera config provider to ExtendedCameraConfigProviderStore
-        extensionsManager.getExtensionEnabledCameraSelector(baseCameraSelector, extensionMode)
-
-        // Injects the new camera config provider which will keep the original extensions needed
-        // configs and also add additional CameraEventMonitor to monitor the camera event callbacks.
-        ExtendedCameraConfigProviderStore.addConfig(cameraEventConfigProviderId) {
-                cameraInfo: CameraInfo, context: Context ->
-            // Retrieves the default extended camera config provider and
-            // ExtensionsUseCaseConfigFactory
-            val defaultCameraConfigProvider =
-                ExtendedCameraConfigProviderStore.getConfigProvider(defaultConfigProviderId)
-            val defaultCameraConfig = defaultCameraConfigProvider.getConfig(cameraInfo, context)!!
-            val defaultExtensionsUseCaseConfigFactory =
-                defaultCameraConfig.retrieveOption(CameraConfig.OPTION_USECASE_CONFIG_FACTORY, null)
-
-            // Creates a new ExtensionsCameraEventMonitorUseCaseConfigFactory on top of the default
-            // ExtensionsCameraEventMonitorUseCaseConfigFactory to monitor the capture session
-            // callbacks
-            val extensionsCameraEventMonitorUseCaseConfigFactory =
-                ExtensionsCameraEventMonitorUseCaseConfigFactory(
-                    defaultExtensionsUseCaseConfigFactory,
-                    cameraEventMonitor
-                )
-
-            // Creates the config from the original config and replaces its use case config factory
-            // with the ExtensionsCameraEventMonitorUseCaseConfigFactory
-            val mutableOptionsBundle = MutableOptionsBundle.from(defaultCameraConfig)
-            mutableOptionsBundle.insertOption(
-                CameraConfig.OPTION_USECASE_CONFIG_FACTORY,
-                extensionsCameraEventMonitorUseCaseConfigFactory
-            )
-
-            // Returns a CameraConfig implemented with the updated config
-            object : CameraConfig {
-                val config = OptionsBundle.from(mutableOptionsBundle)
-
-                override fun getConfig(): Config {
-                    return config
-                }
-
-                override fun getCompatibilityId(): Identifier {
-                    return config.retrieveOption(CameraConfig.OPTION_COMPATIBILITY_ID)!!
-                }
-            }
-        }
-    }
-
-    /**
-     * A ExtensionsCameraEventMonitorCameraFilter which includes the CameraFilter to check whether
-     * the camera is supported for the extension mode or not and also includes the identifier to
-     * find the extended camera config provider from ExtendedCameraConfigProviderStore.
-     */
-    private class ExtensionsCameraEventMonitorCameraFilter constructor(
-        private val extensionManager: ExtensionsManager,
-        @ExtensionMode.Mode private val mode: Int
-    ) : CameraFilter {
-        override fun getIdentifier(): Identifier {
-            return Identifier.create(getCameraEventMonitorCameraConfigProviderId(mode))
-        }
-
-        override fun filter(cameraInfos: MutableList<CameraInfo>): MutableList<CameraInfo> =
-            cameraInfos.mapNotNull { cameraInfo ->
-                val cameraId = Camera2CameraInfo.from(cameraInfo).cameraId
-                val cameraIdCameraSelector = CameraSelectorUtil.createCameraSelectorById(cameraId)
-                if (extensionManager.isExtensionAvailable(cameraIdCameraSelector, mode)) {
-                    cameraInfo
-                } else {
-                    null
-                }
-            }.toMutableList()
-    }
-
-    /**
-     * A UseCaseConfigFactory implemented on top of the default ExtensionsUseCaseConfigFactory to
-     * monitor the camera event callbacks
-     */
-    private class ExtensionsCameraEventMonitorUseCaseConfigFactory constructor(
-        private val useCaseConfigFactory: UseCaseConfigFactory?,
-        private val cameraEventMonitor: CameraEventMonitor
-    ) :
-        UseCaseConfigFactory {
-        override fun getConfig(
-            captureType: UseCaseConfigFactory.CaptureType,
-            captureMode: Int
-        ): Config {
-            // Retrieves the config from the default ExtensionsUseCaseConfigFactory
-            val mutableOptionsBundle = useCaseConfigFactory?.getConfig(
-                captureType, captureMode
-            )?.let {
-                MutableOptionsBundle.from(it)
-            } ?: MutableOptionsBundle.create()
-
-            // Adds the CameraEventMonitor to the original CameraEventCallbacks of ImageCapture to
-            // monitor the camera event callbacks
-            if (captureType.equals(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE)) {
-                var cameraEventCallbacks = mutableOptionsBundle.retrieveOption(
-                    Camera2ImplConfig.CAMERA_EVENT_CALLBACK_OPTION,
-                    null
-                )
-
-                if (cameraEventCallbacks != null) {
-                    cameraEventCallbacks.addAll(
-                        mutableListOf<CameraEventCallback>(
-                            cameraEventMonitor
-                        )
-                    )
-                } else {
-                    cameraEventCallbacks = CameraEventCallbacks(cameraEventMonitor)
-                }
-
-                mutableOptionsBundle.insertOption(
-                    Camera2ImplConfig.CAMERA_EVENT_CALLBACK_OPTION,
-                    cameraEventCallbacks
-                )
-            }
-
-            return OptionsBundle.from(mutableOptionsBundle)
-        }
-    }
-
-    /**
-     * An implementation of CameraEventCallback to monitor whether the camera event callbacks are
-     * called properly or not.
-     */
-    private class CameraEventMonitor : CameraEventCallback() {
+    private class CameraSessionMonitor {
         private var sessionEnabledLatch = CountDownLatch(1)
         private var sessionDisabledLatch = CountDownLatch(1)
 
-        override fun onEnableSession(): CaptureConfig? {
+        fun onOpenedSession() {
             sessionEnabledLatch.countDown()
-            return super.onEnableSession()
         }
 
-        override fun onDisableSession(): CaptureConfig? {
+        fun onClosedSession() {
             sessionDisabledLatch.countDown()
-            return super.onDisableSession()
         }
 
         fun reset() {
@@ -418,11 +278,11 @@
             sessionDisabledLatch = CountDownLatch(1)
         }
 
-        fun awaitSessionEnabledAndAssert() {
+        fun awaitSessionOpenedAndAssert() {
             assertThat(sessionEnabledLatch.await(3000, TimeUnit.MILLISECONDS)).isTrue()
         }
 
-        fun awaitSessionDisabledAndAssert() {
+        fun awaitSessionClosedAndAssert() {
             assertThat(sessionDisabledLatch.await(3000, TimeUnit.MILLISECONDS)).isTrue()
         }
     }
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/car/app/app-automotive/src/main/java/androidx/car/app/hardware/common/PropertyUtils.java b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/common/PropertyUtils.java
index c2ba63e0..1287f7d 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/common/PropertyUtils.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/common/PropertyUtils.java
@@ -333,7 +333,7 @@
     /**
      * Creates a response from {@link CarPropertyValue}.
      */
-    @SuppressWarnings("unchecked")
+    @SuppressWarnings({"unchecked", "deprecation"})
     @NonNull
     @OptIn(markerClass = ExperimentalCarApi.class)
     public static CarPropertyResponse<?> convertPropertyValueToPropertyResponse(
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/hardware/common/MockedCarTestBase.java b/car/app/app-automotive/src/test/java/androidx/car/app/hardware/common/MockedCarTestBase.java
index d5d80e7..d0e8f91 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/hardware/common/MockedCarTestBase.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/hardware/common/MockedCarTestBase.java
@@ -121,6 +121,7 @@
     private CarPropertyManager mCarPropertyManagerMock;
 
     @Before
+    @SuppressWarnings("deprecation")
     public void setUp() {
         // Mock car
         ShadowCar.setCar(mCarMock);
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/hardware/common/PropertyUtilsTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/hardware/common/PropertyUtilsTest.java
index f1b806b..a0bddad 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/hardware/common/PropertyUtilsTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/hardware/common/PropertyUtilsTest.java
@@ -72,6 +72,7 @@
     }
 
     @Test
+    @SuppressWarnings("deprecation")
     public void convertPropertyValueToPropertyResponse_ifStatusAvailableThenAddsValue() {
         when(mCarPropertyValue.getStatus()).thenReturn(CarPropertyValue.STATUS_AVAILABLE);
         assertThat(
@@ -81,6 +82,7 @@
     }
 
     @Test
+    @SuppressWarnings("deprecation")
     public void convertPropertyValueToPropertyResponse_ifStatusIsUnavailableThenIgnoresValue() {
         when(mCarPropertyValue.getStatus()).thenReturn(CarPropertyValue.STATUS_UNAVAILABLE);
         assertThat(
@@ -90,6 +92,7 @@
     }
 
     @Test
+    @SuppressWarnings("deprecation")
     public void convertPropertyValueToPropertyResponse_ifStatusIsErrorThenIgnoresValue() {
         when(mCarPropertyValue.getStatus()).thenReturn(CarPropertyValue.STATUS_ERROR);
         assertThat(
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/MapTemplateWithPaneDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/MapTemplateWithPaneDemoScreen.java
index 520973f..8049bf3 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/MapTemplateWithPaneDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/navigationdemos/MapTemplateWithPaneDemoScreen.java
@@ -179,8 +179,7 @@
                 // Row with a large image.
                 return new Row.Builder()
                         .setTitle(getCarContext().getString(R.string.first_row_title))
-                        .addText(getCarContext().getString(R.string.first_row_text))
-                        .addText(getCarContext().getString(R.string.first_row_text))
+                        .addText(getCarContext().getString(R.string.long_line_text))
                         .setImage(new CarIcon.Builder(mRowLargeIcon).build())
                         .build();
             default:
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/settings/ParkedVsDrivingDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/settings/ParkedVsDrivingDemoScreen.java
index ba198e9..3b10c80 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/settings/ParkedVsDrivingDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/settings/ParkedVsDrivingDemoScreen.java
@@ -90,6 +90,15 @@
                         .addText(getCarContext().getString(R.string.parked_only_text))
                         .build());
 
+        // Add a few rows with long subtext
+        for (int rowIndex = 1; rowIndex < 5; rowIndex++) {
+            listBuilder.addItem(
+                    buildRowForTemplate(
+                            R.string.other_row_title_prefix,
+                            rowIndex,
+                            R.string.long_line_text));
+        }
+
         return new ListTemplate.Builder()
                 .setSingleList(listBuilder.build())
                 .setTitle(getCarContext().getString(R.string.parking_vs_driving_demo_title))
@@ -100,4 +109,12 @@
     private void onClick(String text) {
         CarToast.makeText(getCarContext(), text, LENGTH_LONG).show();
     }
+
+    private Row buildRowForTemplate(int title, int index, int subText) {
+        String rowTitle = getCarContext().getString(title) + " " + (index + 1);
+        return new Row.Builder()
+                .setTitle(rowTitle)
+                .addText(getCarContext().getString(subText))
+                .build();
+    }
 }
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-core/api/current.ignore b/compose/animation/animation-core/api/current.ignore
index 7507c59..ae54e41 100644
--- a/compose/animation/animation-core/api/current.ignore
+++ b/compose/animation/animation-core/api/current.ignore
@@ -3,9 +3,3 @@
     Method androidx.compose.animation.core.InfiniteTransitionKt.animateFloat has changed return type from androidx.compose.runtime.State<java.lang.Float> to androidx.compose.runtime.State<? extends java.lang.Float>
 ChangedType: androidx.compose.animation.core.InfiniteTransitionKt#animateValue(androidx.compose.animation.core.InfiniteTransition, T, T, androidx.compose.animation.core.TwoWayConverter<T,V>, androidx.compose.animation.core.InfiniteRepeatableSpec<T>):
     Method androidx.compose.animation.core.InfiniteTransitionKt.animateValue has changed return type from androidx.compose.runtime.State<T> to androidx.compose.runtime.State<? extends T>
-
-
-InvalidNullConversion: androidx.compose.animation.core.InfiniteTransitionKt#animateFloat(androidx.compose.animation.core.InfiniteTransition, float, float, androidx.compose.animation.core.InfiniteRepeatableSpec<java.lang.Float>):
-    Attempted to remove @NonNull annotation from method androidx.compose.animation.core.InfiniteTransitionKt.animateFloat(androidx.compose.animation.core.InfiniteTransition,float,float,androidx.compose.animation.core.InfiniteRepeatableSpec<java.lang.Float>)
-InvalidNullConversion: androidx.compose.animation.core.InfiniteTransitionKt#animateValue(androidx.compose.animation.core.InfiniteTransition, T, T, androidx.compose.animation.core.TwoWayConverter<T,V>, androidx.compose.animation.core.InfiniteRepeatableSpec<T>):
-    Attempted to remove @NonNull annotation from method androidx.compose.animation.core.InfiniteTransitionKt.animateValue(androidx.compose.animation.core.InfiniteTransition,T,T,androidx.compose.animation.core.TwoWayConverter<T,V>,androidx.compose.animation.core.InfiniteRepeatableSpec<T>)
diff --git a/compose/animation/animation-core/api/restricted_current.ignore b/compose/animation/animation-core/api/restricted_current.ignore
index 7507c59..ae54e41 100644
--- a/compose/animation/animation-core/api/restricted_current.ignore
+++ b/compose/animation/animation-core/api/restricted_current.ignore
@@ -3,9 +3,3 @@
     Method androidx.compose.animation.core.InfiniteTransitionKt.animateFloat has changed return type from androidx.compose.runtime.State<java.lang.Float> to androidx.compose.runtime.State<? extends java.lang.Float>
 ChangedType: androidx.compose.animation.core.InfiniteTransitionKt#animateValue(androidx.compose.animation.core.InfiniteTransition, T, T, androidx.compose.animation.core.TwoWayConverter<T,V>, androidx.compose.animation.core.InfiniteRepeatableSpec<T>):
     Method androidx.compose.animation.core.InfiniteTransitionKt.animateValue has changed return type from androidx.compose.runtime.State<T> to androidx.compose.runtime.State<? extends T>
-
-
-InvalidNullConversion: androidx.compose.animation.core.InfiniteTransitionKt#animateFloat(androidx.compose.animation.core.InfiniteTransition, float, float, androidx.compose.animation.core.InfiniteRepeatableSpec<java.lang.Float>):
-    Attempted to remove @NonNull annotation from method androidx.compose.animation.core.InfiniteTransitionKt.animateFloat(androidx.compose.animation.core.InfiniteTransition,float,float,androidx.compose.animation.core.InfiniteRepeatableSpec<java.lang.Float>)
-InvalidNullConversion: androidx.compose.animation.core.InfiniteTransitionKt#animateValue(androidx.compose.animation.core.InfiniteTransition, T, T, androidx.compose.animation.core.TwoWayConverter<T,V>, androidx.compose.animation.core.InfiniteRepeatableSpec<T>):
-    Attempted to remove @NonNull annotation from method androidx.compose.animation.core.InfiniteTransitionKt.animateValue(androidx.compose.animation.core.InfiniteTransition,T,T,androidx.compose.animation.core.TwoWayConverter<T,V>,androidx.compose.animation.core.InfiniteRepeatableSpec<T>)
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/animation/animation/api/current.ignore b/compose/animation/animation/api/current.ignore
index c62b405..0b4033d 100644
--- a/compose/animation/animation/api/current.ignore
+++ b/compose/animation/animation/api/current.ignore
@@ -1,7 +1,3 @@
 // Baseline format: 1.0
 ChangedType: androidx.compose.animation.TransitionKt#animateColor(androidx.compose.animation.core.InfiniteTransition, long, long, androidx.compose.animation.core.InfiniteRepeatableSpec<androidx.compose.ui.graphics.Color>):
     Method androidx.compose.animation.TransitionKt.animateColor has changed return type from androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> to androidx.compose.runtime.State<? extends androidx.compose.ui.graphics.Color>
-
-
-InvalidNullConversion: androidx.compose.animation.TransitionKt#animateColor(androidx.compose.animation.core.InfiniteTransition, long, long, androidx.compose.animation.core.InfiniteRepeatableSpec<androidx.compose.ui.graphics.Color>):
-    Attempted to remove @NonNull annotation from method androidx.compose.animation.TransitionKt.animateColor(androidx.compose.animation.core.InfiniteTransition,long,long,androidx.compose.animation.core.InfiniteRepeatableSpec<androidx.compose.ui.graphics.Color>)
diff --git a/compose/animation/animation/api/restricted_current.ignore b/compose/animation/animation/api/restricted_current.ignore
index c62b405..0b4033d 100644
--- a/compose/animation/animation/api/restricted_current.ignore
+++ b/compose/animation/animation/api/restricted_current.ignore
@@ -1,7 +1,3 @@
 // Baseline format: 1.0
 ChangedType: androidx.compose.animation.TransitionKt#animateColor(androidx.compose.animation.core.InfiniteTransition, long, long, androidx.compose.animation.core.InfiniteRepeatableSpec<androidx.compose.ui.graphics.Color>):
     Method androidx.compose.animation.TransitionKt.animateColor has changed return type from androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> to androidx.compose.runtime.State<? extends androidx.compose.ui.graphics.Color>
-
-
-InvalidNullConversion: androidx.compose.animation.TransitionKt#animateColor(androidx.compose.animation.core.InfiniteTransition, long, long, androidx.compose.animation.core.InfiniteRepeatableSpec<androidx.compose.ui.graphics.Color>):
-    Attempted to remove @NonNull annotation from method androidx.compose.animation.TransitionKt.animateColor(androidx.compose.animation.core.InfiniteTransition,long,long,androidx.compose.animation.core.InfiniteRepeatableSpec<androidx.compose.ui.graphics.Color>)
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 e2f160a..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
@@ -98,10 +98,13 @@
             8603 to "1.3.1",
             8604 to "1.3.2",
             8605 to "1.3.3",
+            8606 to "1.3.4",
             9000 to "1.4.0-alpha01",
             9001 to "1.4.0-alpha02",
             9100 to "1.4.0-alpha03",
             9200 to "1.4.0-alpha04",
+            9300 to "1.4.0-alpha05",
+            9400 to "1.4.0-alpha06",
         )
 
         /**
@@ -114,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-alpha04"
+        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/build.gradle b/compose/foundation/foundation-newtext/build.gradle
index 1878a73..85d210a 100644
--- a/compose/foundation/foundation-newtext/build.gradle
+++ b/compose/foundation/foundation-newtext/build.gradle
@@ -42,6 +42,7 @@
         implementation(libs.kotlinStdlibCommon)
         implementation(project(':compose:foundation:foundation'))
         implementation(project(":compose:foundation:foundation-layout"))
+        implementation(project(":emoji2:emoji2"))
 
         implementation("androidx.compose.ui:ui-text:1.2.1")
         implementation("androidx.compose.ui:ui-util:1.2.1")
@@ -99,6 +100,11 @@
             }
             androidMain.dependencies {
                 api("androidx.annotation:annotation:1.1.0")
+                implementation(project(":emoji2:emoji2"))
+            }
+
+            desktopMain.dependencies {
+                implementation(libs.kotlinStdlib)
             }
 
             androidTest.dependencies {
diff --git a/compose/foundation/foundation-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/CoreTextInlineContentTest.kt b/compose/foundation/foundation-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/CoreTextInlineContentTest.kt
index f634151..9adc7cd 100644
--- a/compose/foundation/foundation-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/CoreTextInlineContentTest.kt
+++ b/compose/foundation/foundation-newtext/src/androidAndroidTest/kotlin/androidx/compose/foundation/newtext/text/CoreTextInlineContentTest.kt
@@ -48,7 +48,6 @@
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.mock
 import com.nhaarman.mockitokotlin2.verify
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -66,7 +65,6 @@
 
     @OptIn(ExperimentalTextApi::class)
     @Test
-    @Ignore // not implemented yet
     fun placeholder_changeSize_updateInlineContentSize() {
         // Callback to monitor the size changes of a composable.
         val onSizeChanged: (IntSize) -> Unit = mock()
@@ -113,7 +111,6 @@
     }
 
     @Test
-    @Ignore // not implemented yet
     fun rtlLayout_inlineContent_placement() {
         rule.setContent {
             CompositionLocalProvider(
@@ -133,7 +130,6 @@
     }
 
     @Test
-    @Ignore // not implemented yet
     fun rtlTextContent_inlineContent_placement() {
         rule.setContent {
             // RTL character, supported by sample_font
@@ -149,7 +145,6 @@
     }
 
     @Test
-    @Ignore // not implemented yet
     fun rtlTextDirection_inlineContent_placement() {
         rule.setContent {
             // LTR character, supported by sample_font
@@ -166,7 +161,6 @@
     }
 
     @Test
-    @Ignore // not implemented yet
     fun bidiText_inlineContent_placement() {
         rule.setContent {
             // RTL and LTR characters, supported by sample_font
@@ -182,7 +176,6 @@
     }
 
     @Test
-    @Ignore // not implemented yet
     fun bidiText_2_inlineContent_placement() {
         rule.setContent {
             // RTL and LTR characters, supported by sample_font
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 d5780c1..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(
-                TextInlineContentLayoutDrawParams(
-                    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(
-                TextInlineContentLayoutDrawParams(
-                    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(
-            TextInlineContentLayoutDrawParams(
-                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(
-            TextInlineContentLayoutDrawParams(
-                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(
-            TextInlineContentLayoutDrawParams(
-                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(
-            TextInlineContentLayoutDrawParams(
-                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(
-            TextInlineContentLayoutDrawParams(
-                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 e0edf29..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(
-            TextInlineContentLayoutDrawParams(
-                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 3adb8f6d..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(
-                TextInlineContentLayoutDrawParams(
-                    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(
-            TextInlineContentLayoutDrawParams(
-                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(
-                TextInlineContentLayoutDrawParams(
-                    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(
-            TextInlineContentLayoutDrawParams(
-                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(
-            TextInlineContentLayoutDrawParams(
-                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(
-                TextInlineContentLayoutDrawParams(
-                    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(
-            TextInlineContentLayoutDrawParams(
-                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(
-            TextInlineContentLayoutDrawParams(
-                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(
-            TextInlineContentLayoutDrawParams(
-                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(
-            TextInlineContentLayoutDrawParams(
-                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(
-            TextInlineContentLayoutDrawParams(
-                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/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/ContextMenu.android.kt b/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/ContextMenu.android.kt
new file mode 100644
index 0000000..f907321
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/ContextMenu.android.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.newtext.text.copypasta
+
+import androidx.compose.foundation.newtext.text.copypasta.selection.SelectionManager
+import androidx.compose.runtime.Composable
+
+@Composable
+internal actual fun ContextMenuArea(
+    manager: SelectionManager,
+    content: @Composable () -> Unit
+) {
+    content()
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/StringHelpers.android.kt b/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/StringHelpers.android.kt
new file mode 100644
index 0000000..92cb068
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/StringHelpers.android.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.newtext.text.copypasta
+
+import androidx.emoji2.text.EmojiCompat
+import java.text.BreakIterator
+
+internal actual fun String.findPrecedingBreak(index: Int): Int {
+    val emojiBreak =
+        getEmojiCompatIfLoaded()?.getEmojiStart(this, maxOf(0, index - 1))?.takeUnless { it == -1 }
+    if (emojiBreak != null) return emojiBreak
+
+    val it = BreakIterator.getCharacterInstance()
+    it.setText(this)
+    return it.preceding(index)
+}
+
+internal actual fun String.findFollowingBreak(index: Int): Int {
+    val emojiBreak = getEmojiCompatIfLoaded()?.getEmojiEnd(this, index)?.takeUnless { it == -1 }
+    if (emojiBreak != null) return emojiBreak
+
+    val it = BreakIterator.getCharacterInstance()
+    it.setText(this)
+    return it.following(index)
+}
+
+private fun getEmojiCompatIfLoaded(): EmojiCompat? =
+    if (EmojiCompat.isConfigured())
+        EmojiCompat.get().takeIf { it.loadState == EmojiCompat.LOAD_STATE_SUCCEEDED }
+    else null
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/TextPointerIcon.android.kt b/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/TextPointerIcon.android.kt
new file mode 100644
index 0000000..6645638
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/TextPointerIcon.android.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.newtext.text.copypasta
+
+import androidx.compose.ui.input.pointer.PointerIcon
+
+internal actual val textPointerIcon: PointerIcon =
+    PointerIcon(android.view.PointerIcon.TYPE_TEXT)
diff --git a/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/AndroidSelectionHandles.android.kt b/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/AndroidSelectionHandles.android.kt
new file mode 100644
index 0000000..ab50488
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/AndroidSelectionHandles.android.kt
@@ -0,0 +1,327 @@
+/*
+ * 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.newtext.text.copypasta.selection
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.newtext.text.copypasta.selection.HandleReferencePoint.TopLeft
+import androidx.compose.foundation.newtext.text.copypasta.selection.HandleReferencePoint.TopMiddle
+import androidx.compose.foundation.newtext.text.copypasta.selection.HandleReferencePoint.TopRight
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.CacheDrawScope
+import androidx.compose.ui.draw.drawWithCache
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.ImageBitmapConfig
+import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
+import androidx.compose.ui.graphics.drawscope.scale
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.PopupPositionProvider
+import androidx.compose.ui.window.PopupProperties
+import kotlin.math.ceil
+import kotlin.math.roundToInt
+
+@Composable
+internal actual fun SelectionHandle(
+    position: Offset,
+    isStartHandle: Boolean,
+    direction: ResolvedTextDirection,
+    handlesCrossed: Boolean,
+    modifier: Modifier,
+    content: @Composable (() -> Unit)?
+) {
+    val isLeft = isLeft(isStartHandle, direction, handlesCrossed)
+    // The left selection handle's top right is placed at the given position, and vice versa.
+    val handleReferencePoint = if (isLeft) {
+        TopRight
+    } else {
+        TopLeft
+    }
+
+    HandlePopup(position = position, handleReferencePoint = handleReferencePoint) {
+        if (content == null) {
+            DefaultSelectionHandle(
+                modifier = modifier
+                    .semantics {
+                        this[SelectionHandleInfoKey] = SelectionHandleInfo(
+                            position = position
+                        )
+                    },
+                isStartHandle = isStartHandle,
+                direction = direction,
+                handlesCrossed = handlesCrossed
+            )
+        } else {
+            content()
+        }
+    }
+}
+
+@Composable
+/*@VisibleForTesting*/
+internal fun DefaultSelectionHandle(
+    modifier: Modifier,
+    isStartHandle: Boolean,
+    direction: ResolvedTextDirection,
+    handlesCrossed: Boolean
+) {
+    Spacer(
+        modifier.size(
+            HandleWidth,
+            HandleHeight
+        )
+            .drawSelectionHandle(isStartHandle, direction, handlesCrossed)
+    )
+}
+
+@Suppress("ModifierInspectorInfo")
+internal fun Modifier.drawSelectionHandle(
+    isStartHandle: Boolean,
+    direction: ResolvedTextDirection,
+    handlesCrossed: Boolean
+) = composed {
+    val handleColor = LocalTextSelectionColors.current.handleColor
+    this.then(
+        Modifier.drawWithCache {
+            val radius = size.width / 2f
+            val handleImage = createHandleImage(radius)
+            val colorFilter = ColorFilter.tint(handleColor)
+            onDrawWithContent {
+                drawContent()
+                val isLeft = isLeft(isStartHandle, direction, handlesCrossed)
+                if (isLeft) {
+                    // Flip the selection handle horizontally.
+                    scale(scaleX = -1f, scaleY = 1f) {
+                        drawImage(
+                            image = handleImage,
+                            colorFilter = colorFilter
+                        )
+                    }
+                } else {
+                    drawImage(
+                        image = handleImage,
+                        colorFilter = colorFilter
+                    )
+                }
+            }
+        }
+    )
+}
+
+/**
+ * The cache for the image mask created to draw selection/cursor handle, so that we don't need to
+ * recreate them.
+ */
+private object HandleImageCache {
+    var imageBitmap: ImageBitmap? = null
+    var canvas: Canvas? = null
+    var canvasDrawScope: CanvasDrawScope? = null
+}
+
+/**
+ * Create an image bitmap for the basic shape of a selection handle or cursor handle. It is an
+ * circle with a rectangle covering its left top part.
+ *
+ * To draw the right selection handle, directly draw this image bitmap.
+ * To draw the left selection handle, mirror the canvas first and then draw this image bitmap.
+ * To draw the cursor handle, translate and rotated the canvas 45 degrees, then draw this image
+ * bitmap.
+ *
+ * @param radius the radius of circle in selection/cursor handle.
+ * CanvasDrawScope objects so that we only recreate them when necessary.
+ */
+internal fun CacheDrawScope.createHandleImage(radius: Float): ImageBitmap {
+    // The edge length of the square bounding box of the selection/cursor handle. This is also
+    // the size of the bitmap needed for the bitmap mask.
+    val edge = ceil(radius).toInt() * 2
+
+    var imageBitmap = HandleImageCache.imageBitmap
+    var canvas = HandleImageCache.canvas
+    var drawScope = HandleImageCache.canvasDrawScope
+
+    // If the cached bitmap is null or too small, we need to create new bitmap.
+    if (
+        imageBitmap == null ||
+        canvas == null ||
+        edge > imageBitmap.width ||
+        edge > imageBitmap.height
+    ) {
+        imageBitmap = ImageBitmap(
+            width = edge,
+            height = edge,
+            config = ImageBitmapConfig.Alpha8
+        )
+        HandleImageCache.imageBitmap = imageBitmap
+        canvas = Canvas(imageBitmap)
+        HandleImageCache.canvas = canvas
+    }
+    if (drawScope == null) {
+        drawScope = CanvasDrawScope()
+        HandleImageCache.canvasDrawScope = drawScope
+    }
+
+    drawScope.draw(
+        this,
+        layoutDirection,
+        canvas,
+        Size(imageBitmap.width.toFloat(), imageBitmap.height.toFloat())
+    ) {
+        // Clear the previously rendered portion within this ImageBitmap as we could
+        // be re-using it
+        drawRect(
+            color = Color.Black,
+            size = size,
+            blendMode = BlendMode.Clear
+        )
+
+        // Draw the rectangle at top left.
+        drawRect(
+            color = Color(0xFF000000),
+            topLeft = Offset.Zero,
+            size = Size(radius, radius)
+        )
+        // Draw the circle
+        drawCircle(
+            color = Color(0xFF000000),
+            radius = radius,
+            center = Offset(radius, radius)
+        )
+    }
+    return imageBitmap
+}
+
+@Composable
+internal fun HandlePopup(
+    position: Offset,
+    handleReferencePoint: HandleReferencePoint,
+    content: @Composable () -> Unit
+) {
+    val intOffset = IntOffset(position.x.roundToInt(), position.y.roundToInt())
+
+    val popupPositioner = remember(handleReferencePoint, intOffset) {
+        HandlePositionProvider(handleReferencePoint, intOffset)
+    }
+
+    Popup(
+        popupPositionProvider = popupPositioner,
+        properties = PopupProperties(
+            excludeFromSystemGesture = true,
+            clippingEnabled = false
+        ),
+        content = content
+    )
+}
+
+/**
+ * The enum that specifies how a selection/cursor handle is placed to its given position.
+ * When this value is [TopLeft], the top left corner of the handle will be placed at the
+ * given position.
+ * When this value is [TopRight], the top right corner of the handle will be placed at the
+ * given position.
+ * When this value is [TopMiddle], the handle top edge's middle point will be placed at the given
+ * position.
+ */
+internal enum class HandleReferencePoint {
+    TopLeft,
+    TopRight,
+    TopMiddle
+}
+
+/**
+ * This [PopupPositionProvider] for [HandlePopup]. It will position the selection handle
+ * to the [offset] in its anchor layout.
+ *
+ * @see HandleReferencePoint
+ */
+/*@VisibleForTesting*/
+internal class HandlePositionProvider(
+    private val handleReferencePoint: HandleReferencePoint,
+    private val offset: IntOffset
+) : PopupPositionProvider {
+    override fun calculatePosition(
+        anchorBounds: IntRect,
+        windowSize: IntSize,
+        layoutDirection: LayoutDirection,
+        popupContentSize: IntSize
+    ): IntOffset {
+        return when (handleReferencePoint) {
+            TopLeft ->
+                IntOffset(
+                    x = anchorBounds.left + offset.x,
+                    y = anchorBounds.top + offset.y
+                )
+            TopRight ->
+                IntOffset(
+                    x = anchorBounds.left + offset.x - popupContentSize.width,
+                    y = anchorBounds.top + offset.y
+                )
+            TopMiddle ->
+                IntOffset(
+                    x = anchorBounds.left + offset.x - popupContentSize.width / 2,
+                    y = anchorBounds.top + offset.y
+                )
+        }
+    }
+}
+
+/**
+ * Computes whether the handle's appearance should be left-pointing or right-pointing.
+ */
+private fun isLeft(
+    isStartHandle: Boolean,
+    direction: ResolvedTextDirection,
+    handlesCrossed: Boolean
+): Boolean {
+    return if (isStartHandle) {
+        isHandleLtrDirection(direction, handlesCrossed)
+    } else {
+        !isHandleLtrDirection(direction, handlesCrossed)
+    }
+}
+
+/**
+ * This method is to check if the selection handles should use the natural Ltr pointing
+ * direction.
+ * If the context is Ltr and the handles are not crossed, or if the context is Rtl and the handles
+ * are crossed, return true.
+ *
+ * In Ltr context, the start handle should point to the left, and the end handle should point to
+ * the right. However, in Rtl context or when handles are crossed, the start handle should point to
+ * the right, and the end handle should point to left.
+ */
+/*@VisibleForTesting*/
+internal fun isHandleLtrDirection(
+    direction: ResolvedTextDirection,
+    areHandlesCrossed: Boolean
+): Boolean {
+    return direction == ResolvedTextDirection.Ltr && !areHandlesCrossed ||
+        direction == ResolvedTextDirection.Rtl && areHandlesCrossed
+}
diff --git a/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionManager.android.kt b/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionManager.android.kt
new file mode 100644
index 0000000..7a7e170
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/androidMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionManager.android.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.newtext.text.copypasta.selection
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.MagnifierStyle
+import androidx.compose.foundation.magnifier
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.input.key.KeyEvent
+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
+
+// We use composed{} to read a local, but don't provide inspector info because the underlying
+// magnifier modifier provides more meaningful inspector info.
+@SuppressLint("ModifierInspectorInfo")
+@OptIn(ExperimentalFoundationApi::class)
+internal actual fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier {
+    // Avoid tracking animation state on older Android versions that don't support magnifiers.
+    if (!MagnifierStyle.TextDefault.isSupported) {
+        return this
+    }
+
+    return composed {
+        val density = LocalDensity.current
+        var magnifierSize by remember { mutableStateOf(IntSize.Zero) }
+        animatedSelectionMagnifier(
+            magnifierCenter = {
+                calculateSelectionMagnifierCenterAndroid(
+                    manager,
+                    magnifierSize
+                )
+            },
+            platformMagnifier = { center ->
+                Modifier
+                    .magnifier(
+                        sourceCenter = { center() },
+                        onSizeChanged = { size ->
+                            magnifierSize = with(density) {
+                                IntSize(size.width.roundToPx(), size.height.roundToPx())
+                            }
+                        },
+                        style = MagnifierStyle.TextDefault
+                    )
+            }
+        )
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/AnnotatedStringResolveInlineContent.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/AnnotatedStringResolveInlineContent.kt
index 39cfec4..26ebe38 100644
--- a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/AnnotatedStringResolveInlineContent.kt
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/AnnotatedStringResolveInlineContent.kt
@@ -62,6 +62,9 @@
     return Pair(placeholders, inlineComposables)
 }
 
+internal fun AnnotatedString.hasInlineContent(): Boolean =
+    hasStringAnnotations(INLINE_CONTENT_TAG, 0, text.length)
+
 @Composable
 internal fun InlineChildren(
     text: AnnotatedString,
@@ -81,7 +84,7 @@
 
 private typealias PlaceholderRange = AnnotatedString.Range<Placeholder>
 private typealias InlineContentRange = AnnotatedString.Range<@Composable (String) -> Unit>
-internal const val INLINE_CONTENT_TAG = "androidx.compose.foundation.newtext.text.inlineContent"
+internal const val INLINE_CONTENT_TAG = "androidx.compose.foundation.text.inlineContent"
 
 private val EmptyInlineContent: Pair<List<PlaceholderRange>, List<InlineContentRange>> =
     Pair(emptyList(), emptyList())
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 850a7e0..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
@@ -16,8 +16,13 @@
 
 package androidx.compose.foundation.newtext.text
 
-import androidx.compose.foundation.newtext.text.modifiers.StaticTextModifier
-import androidx.compose.foundation.newtext.text.modifiers.TextInlineContentLayoutDrawParams
+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.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
@@ -30,9 +35,8 @@
 import androidx.compose.ui.layout.MeasurePolicy
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.layout.Placeable
 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
@@ -49,21 +53,34 @@
 /**
  * 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,
 ) {
-    Layout(
-        content = {},
-        modifier = modifier.textModifier(
+    validateMinMaxLines(minLines, maxLines)
+    val selectionRegistrar = LocalSelectionRegistrar.current
+    val selectionController = if (selectionRegistrar != null) {
+        val backgroundSelectionColor = LocalTextSelectionColors.current.backgroundColor
+        remember(selectionRegistrar, backgroundSelectionColor) {
+            SelectionController(
+                selectionRegistrar,
+                backgroundSelectionColor
+            )
+        }
+    } else {
+        null
+    }
+    val finalModifier = if (selectionController != null || onTextLayout != null) {
+        modifier.textModifier(
             AnnotatedString(text),
             style = style,
             onTextLayout = onTextLayout,
@@ -74,9 +91,20 @@
             fontFamilyResolver = LocalFontFamilyResolver.current,
             placeholders = null,
             onPlaceholderLayout = null,
-        ),
-        TextMeasurePolicy { null }
-    )
+            selectionController = selectionController
+        )
+    } else {
+        modifier then TextStringSimpleElement(
+            text,
+            style,
+            LocalFontFamilyResolver.current,
+            overflow,
+            softWrap,
+            maxLines,
+            minLines
+        )
+    }
+    Layout(finalModifier, EmptyMeasurePolicy)
 }
 
 /**
@@ -95,32 +123,72 @@
     minLines: Int = 1,
     inlineContent: Map<String, InlineTextContent>? = null,
 ) {
-    val fontFamilyResolver = LocalFontFamilyResolver.current
-
-    val (placeholders, inlineComposables) = text.resolveInlineContent(inlineContent)
-    val measuredPlaceholderPositions = remember {
-        mutableStateOf<List<Rect?>?>(null)
+    validateMinMaxLines(minLines, maxLines)
+    val selectionRegistrar = LocalSelectionRegistrar.current
+    val selectionController = if (selectionRegistrar != null) {
+        val backgroundSelectionColor = LocalTextSelectionColors.current.backgroundColor
+        remember(selectionRegistrar, backgroundSelectionColor) {
+            SelectionController(
+                selectionRegistrar,
+                backgroundSelectionColor
+            )
+        }
+    } else {
+        null
     }
-    Layout(
-        content = if (inlineComposables.isEmpty()) {
-            {}
-        } else {
-            { InlineChildren(text, inlineComposables) }
-        },
-        modifier = modifier.textModifier(
-            text = text,
-            style = style,
-            onTextLayout = onTextLayout,
-            overflow = overflow,
-            softWrap = softWrap,
-            maxLines = maxLines,
-            minLines = minLines,
-            fontFamilyResolver = fontFamilyResolver,
-            placeholders = placeholders,
-            onPlaceholderLayout = { measuredPlaceholderPositions.value = it }
-        ),
-        measurePolicy = TextMeasurePolicy { measuredPlaceholderPositions.value }
-    )
+
+    if (!text.hasInlineContent()) {
+        // this is the same as text: String, use all the early exits
+        Layout(
+            modifier = modifier.textModifier(
+                text = text,
+                style = style,
+                onTextLayout = onTextLayout,
+                overflow = overflow,
+                softWrap = softWrap,
+                maxLines = maxLines,
+                minLines = minLines,
+                fontFamilyResolver = LocalFontFamilyResolver.current,
+                placeholders = null,
+                onPlaceholderLayout = null,
+                selectionController = selectionController
+            ),
+            EmptyMeasurePolicy
+        )
+    } else {
+        // do the inline content allocs
+        val (placeholders, inlineComposables) = text.resolveInlineContent(inlineContent)
+        val measuredPlaceholderPositions = remember {
+            mutableStateOf<List<Rect?>?>(null)
+        }
+        Layout(
+            content = { InlineChildren(text, inlineComposables) },
+            modifier = modifier.textModifier(
+                text = text,
+                style = style,
+                onTextLayout = onTextLayout,
+                overflow = overflow,
+                softWrap = softWrap,
+                maxLines = maxLines,
+                minLines = minLines,
+                fontFamilyResolver = LocalFontFamilyResolver.current,
+                placeholders = placeholders,
+                onPlaceholderLayout = { measuredPlaceholderPositions.value = it },
+                selectionController = selectionController
+            ),
+            measurePolicy = TextMeasurePolicy { measuredPlaceholderPositions.value }
+        )
+    }
+}
+
+private object EmptyMeasurePolicy : MeasurePolicy {
+    private val placementBlock: Placeable.PlacementScope.() -> Unit = {}
+    override fun MeasureScope.measure(
+        measurables: List<Measurable>,
+        constraints: Constraints
+    ): MeasureResult {
+        return layout(constraints.maxWidth, constraints.maxHeight, placementBlock = placementBlock)
+    }
 }
 
 private class TextMeasurePolicy(
@@ -167,27 +235,38 @@
     minLines: Int,
     fontFamilyResolver: FontFamily.Resolver,
     placeholders: List<AnnotatedString.Range<Placeholder>>?,
-    onPlaceholderLayout: ((List<Rect?>) -> Unit)?
+    onPlaceholderLayout: ((List<Rect?>) -> Unit)?,
+    selectionController: SelectionController?
 ): Modifier {
-    val params = TextInlineContentLayoutDrawParams(
-        text,
-        style,
-        fontFamilyResolver,
-        onTextLayout,
-        overflow,
-        softWrap,
-        maxLines,
-        minLines,
-        placeholders,
-        onPlaceholderLayout,
-    )
-    return this then object : ModifierNodeElement<StaticTextModifier>(
-        params,
-        false,
-        debugInspectorInfo {}
-    ) {
-        override fun create(): StaticTextModifier = StaticTextModifier(params)
-        override fun update(node: StaticTextModifier): StaticTextModifier =
-            node.also { it.update(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
     }
 }
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/ContextMenu.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/ContextMenu.kt
new file mode 100644
index 0000000..b42ab4e
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/ContextMenu.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.newtext.text.copypasta
+
+import androidx.compose.foundation.newtext.text.copypasta.selection.SelectionManager
+import androidx.compose.runtime.Composable
+
+@Composable
+internal expect fun ContextMenuArea(
+    manager: SelectionManager,
+    content: @Composable () -> Unit
+)
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/Expect.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/Expect.kt
new file mode 100644
index 0000000..ea9d587
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/Expect.kt
@@ -0,0 +1,25 @@
+// ktlint-disable filename
+
+/*
+ * 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.newtext.text.copypasta
+
+expect class AtomicLong(value: Long) {
+    fun get(): Long
+    fun set(value: Long)
+    fun getAndIncrement(): Long
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/LongPressTextDragObserver.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/LongPressTextDragObserver.kt
new file mode 100644
index 0000000..934fa91
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/LongPressTextDragObserver.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.newtext.text.copypasta
+
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.util.fastAny
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+internal interface TextDragObserver {
+    /**
+     * Called as soon as a down event is received. If the pointer eventually moves while remaining
+     * down, a drag gesture may be started. After this method:
+     * - [onUp] will always be called eventually, once the pointer is released.
+     * - [onStart] _may_ be called, if there is a drag that exceeds touch slop.
+     *
+     * This method will not be called before [onStart] in the case when a down event happens that
+     * may not result in a drag, e.g. on the down before a long-press that starts a selection.
+     */
+    fun onDown(point: Offset)
+
+    /**
+     * Called after [onDown] if an up event is received without dragging.
+     */
+    fun onUp()
+
+    /**
+     * Called once a drag gesture has started, which means touch slop has been exceeded.
+     * [onDown] _may_ be called before this method if the down event could not have
+     * started a different gesture.
+     */
+    fun onStart(startPoint: Offset)
+
+    fun onDrag(delta: Offset)
+
+    fun onStop()
+
+    fun onCancel()
+}
+
+internal suspend fun PointerInputScope.detectDragGesturesAfterLongPressWithObserver(
+    observer: TextDragObserver
+) = detectDragGesturesAfterLongPress(
+    onDragEnd = { observer.onStop() },
+    onDrag = { _, offset ->
+        observer.onDrag(offset)
+    },
+    onDragStart = {
+        observer.onStart(it)
+    },
+    onDragCancel = { observer.onCancel() }
+)
+
+/**
+ * Detects gesture events for a [TextDragObserver], including both initial down events and drag
+ * events.
+ */
+internal suspend fun PointerInputScope.detectDownAndDragGesturesWithObserver(
+    observer: TextDragObserver
+) {
+    coroutineScope {
+        launch {
+            detectPreDragGesturesWithObserver(observer)
+        }
+        launch {
+            detectDragGesturesWithObserver(observer)
+        }
+    }
+}
+
+/**
+ * Detects initial down events and calls [TextDragObserver.onDown] and
+ * [TextDragObserver.onUp].
+ */
+private suspend fun PointerInputScope.detectPreDragGesturesWithObserver(
+    observer: TextDragObserver
+) {
+    awaitEachGesture {
+        val down = awaitFirstDown()
+        observer.onDown(down.position)
+        // Wait for that pointer to come up.
+        do {
+            val event = awaitPointerEvent()
+        } while (event.changes.fastAny { it.id == down.id && it.pressed })
+        observer.onUp()
+    }
+}
+
+/**
+ * Detects drag gestures for a [TextDragObserver].
+ */
+private suspend fun PointerInputScope.detectDragGesturesWithObserver(
+    observer: TextDragObserver
+) {
+    detectDragGestures(
+        onDragEnd = { observer.onStop() },
+        onDrag = { _, offset ->
+            observer.onDrag(offset)
+        },
+        onDragStart = {
+            observer.onStart(it)
+        },
+        onDragCancel = { observer.onCancel() }
+    )
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/StringHelpers.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/StringHelpers.kt
new file mode 100644
index 0000000..a63f8bf
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/StringHelpers.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.newtext.text.copypasta
+
+import androidx.compose.ui.text.TextRange
+
+/**
+ * Returns the index of the character break preceding [index].
+ */
+internal expect fun String.findPrecedingBreak(index: Int): Int
+
+/**
+ * Returns the index of the character break following [index]. Returns -1 if there are no more
+ * breaks before the end of the string.
+ */
+internal expect fun String.findFollowingBreak(index: Int): Int
+
+internal fun CharSequence.findParagraphStart(startIndex: Int): Int {
+    for (index in startIndex - 1 downTo 1) {
+        if (this[index - 1] == '\n') {
+            return index
+        }
+    }
+    return 0
+}
+
+internal fun CharSequence.findParagraphEnd(startIndex: Int): Int {
+    for (index in startIndex + 1 until this.length) {
+        if (this[index] == '\n') {
+            return index
+        }
+    }
+    return this.length
+}
+
+/**
+ * Returns the text range of the paragraph at the given character offset.
+ *
+ * Paragraphs are separated by Line Feed character (\n).
+ */
+internal fun CharSequence.getParagraphBoundary(index: Int): TextRange {
+    return TextRange(findParagraphStart(index), findParagraphEnd(index))
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/TextLayoutResultProxy.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/TextLayoutResultProxy.kt
new file mode 100644
index 0000000..36d962b
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/TextLayoutResultProxy.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.copypasta
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.TextLayoutResult
+
+internal class TextLayoutResultProxy(val value: TextLayoutResult) {
+    // TextLayoutResult methods
+    /**
+     * Translates the position of the touch on the screen to the position in text. Because touch
+     * is relative to the decoration box, we need to translate it to the inner text field's
+     * coordinates first before calculating position of the symbol in text.
+     *
+     * @param position original position of the gesture relative to the decoration box
+     * @param coerceInVisibleBounds if true and original [position] is outside visible bounds
+     * of the inner text field, the [position] will be shifted to the closest edge of the inner
+     * text field's visible bounds. This is useful when you have a decoration box
+     * bigger than the inner text field, so when user touches to the decoration box area, the cursor
+     * goes to the beginning or the end of the visible inner text field; otherwise if we put the
+     * cursor under the touch in the invisible part of the inner text field, it would scroll to
+     * make the cursor visible. This behavior is not needed, and therefore
+     * [coerceInVisibleBounds] should be set to false, when the user drags outside visible bounds
+     * to make a selection.
+     */
+    fun getOffsetForPosition(position: Offset, coerceInVisibleBounds: Boolean = true): Int {
+        val relativePosition = position
+            .let { if (coerceInVisibleBounds) it.coercedInVisibleBoundsOfInputText() else it }
+            .relativeToInputText()
+        return value.getOffsetForPosition(relativePosition)
+    }
+
+    fun getLineForVerticalPosition(vertical: Float): Int {
+        val relativeVertical = Offset(0f, vertical)
+            .coercedInVisibleBoundsOfInputText()
+            .relativeToInputText().y
+        return value.getLineForVerticalPosition(relativeVertical)
+    }
+
+    fun getLineEnd(lineIndex: Int, visibleEnd: Boolean = false): Int =
+        value.getLineEnd(lineIndex, visibleEnd)
+
+    /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
+     * in the view. Returns false when the position is in the empty space of left/right of text.
+     */
+    fun isPositionOnText(offset: Offset): Boolean {
+        val relativeOffset = offset.coercedInVisibleBoundsOfInputText().relativeToInputText()
+        val line = value.getLineForVerticalPosition(relativeOffset.y)
+        return relativeOffset.x >= value.getLineLeft(line) &&
+            relativeOffset.x <= value.getLineRight(line)
+    }
+
+    // Shift offset
+    /** Measured bounds of the decoration box and inner text field. Together used to
+     * calculate the relative touch offset. Because touches are applied on the decoration box, we
+     * need to translate it to the inner text field coordinates.
+     */
+    var innerTextFieldCoordinates: LayoutCoordinates? = null
+    var decorationBoxCoordinates: LayoutCoordinates? = null
+
+    /**
+     * Translates the click happened on the decoration box to the position in the inner text
+     * field coordinates. This relative position is then used to determine symbol position in
+     * text using TextLayoutResult object.
+     */
+    private fun Offset.relativeToInputText(): Offset {
+        // Translates touch to the inner text field coordinates
+        return innerTextFieldCoordinates?.let { innerTextFieldCoordinates ->
+            decorationBoxCoordinates?.let { decorationBoxCoordinates ->
+                if (innerTextFieldCoordinates.isAttached && decorationBoxCoordinates.isAttached) {
+                    innerTextFieldCoordinates.localPositionOf(decorationBoxCoordinates, this)
+                } else {
+                    this
+                }
+            }
+        } ?: this
+    }
+
+    /**
+     * If click on the decoration box happens outside visible inner text field, coerce the click
+     * position to the visible edges of the inner text field.
+     */
+    private fun Offset.coercedInVisibleBoundsOfInputText(): Offset {
+        // If offset is outside visible bounds of the inner text field, use visible bounds edges
+        val visibleInnerTextFieldRect =
+            innerTextFieldCoordinates?.let { innerTextFieldCoordinates ->
+                if (innerTextFieldCoordinates.isAttached) {
+                    decorationBoxCoordinates?.localBoundingBoxOf(innerTextFieldCoordinates)
+                } else {
+                    Rect.Zero
+                }
+            } ?: Rect.Zero
+        return this.coerceIn(visibleInnerTextFieldRect)
+    }
+}
+
+private fun Offset.coerceIn(rect: Rect): Offset {
+    val xOffset = when {
+        x < rect.left -> rect.left
+        x > rect.right -> rect.right
+        else -> x
+    }
+    val yOffset = when {
+        y < rect.top -> rect.top
+        y > rect.bottom -> rect.bottom
+        else -> y
+    }
+    return Offset(xOffset, yOffset)
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/TextPointerIcon.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/TextPointerIcon.kt
new file mode 100644
index 0000000..3d299bc
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/TextPointerIcon.kt
@@ -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.
+ */
+
+package androidx.compose.foundation.newtext.text.copypasta
+
+import androidx.compose.ui.input.pointer.PointerIcon
+
+internal expect val textPointerIcon: PointerIcon
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/TouchMode.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/TouchMode.kt
new file mode 100644
index 0000000..54ee1ec
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/TouchMode.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.compose.foundation.newtext.text.copypasta
+
+/**
+ * This is a temporary workaround and should be removed after proper mouse handling is settled
+ * (b/171402426).
+ */
+internal val isInTouchMode: Boolean = true
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/MultiWidgetSelectionDelegate.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/MultiWidgetSelectionDelegate.kt
new file mode 100644
index 0000000..b234d34
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/MultiWidgetSelectionDelegate.kt
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.newtext.text.copypasta.selection
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
+import kotlin.math.max
+
+internal class MultiWidgetSelectionDelegate(
+    override val selectableId: Long,
+    private val coordinatesCallback: () -> LayoutCoordinates?,
+    private val layoutResultCallback: () -> TextLayoutResult?
+) : Selectable {
+
+    override fun updateSelection(
+        startHandlePosition: Offset,
+        endHandlePosition: Offset,
+        previousHandlePosition: Offset?,
+        isStartHandle: Boolean,
+        containerLayoutCoordinates: LayoutCoordinates,
+        adjustment: SelectionAdjustment,
+        previousSelection: Selection?
+    ): Pair<Selection?, Boolean> {
+        require(
+            previousSelection == null || (
+                selectableId == previousSelection.start.selectableId &&
+                    selectableId == previousSelection.end.selectableId
+                )
+        ) {
+            "The given previousSelection doesn't belong to this selectable."
+        }
+        val layoutCoordinates = getLayoutCoordinates() ?: return Pair(null, false)
+        val textLayoutResult = layoutResultCallback() ?: return Pair(null, false)
+
+        val relativePosition = containerLayoutCoordinates.localPositionOf(
+            layoutCoordinates, Offset.Zero
+        )
+        val localStartPosition = startHandlePosition - relativePosition
+        val localEndPosition = endHandlePosition - relativePosition
+        val localPreviousHandlePosition = previousHandlePosition?.let { it - relativePosition }
+
+        return getTextSelectionInfo(
+            textLayoutResult = textLayoutResult,
+            startHandlePosition = localStartPosition,
+            endHandlePosition = localEndPosition,
+            previousHandlePosition = localPreviousHandlePosition,
+            selectableId = selectableId,
+            adjustment = adjustment,
+            previousSelection = previousSelection,
+            isStartHandle = isStartHandle
+        )
+    }
+
+    override fun getSelectAllSelection(): Selection? {
+        val textLayoutResult = layoutResultCallback() ?: return null
+        val newSelectionRange = TextRange(0, textLayoutResult.layoutInput.text.length)
+
+        return getAssembledSelectionInfo(
+            newSelectionRange = newSelectionRange,
+            handlesCrossed = false,
+            selectableId = selectableId,
+            textLayoutResult = textLayoutResult
+        )
+    }
+
+    override fun getHandlePosition(selection: Selection, isStartHandle: Boolean): Offset {
+        // Check if the selection handle's selectable is the current selectable.
+        if (isStartHandle && selection.start.selectableId != this.selectableId ||
+            !isStartHandle && selection.end.selectableId != this.selectableId
+        ) {
+            return Offset.Zero
+        }
+
+        if (getLayoutCoordinates() == null) return Offset.Zero
+
+        val textLayoutResult = layoutResultCallback() ?: return Offset.Zero
+        return getSelectionHandleCoordinates(
+            textLayoutResult = textLayoutResult,
+            offset = if (isStartHandle) selection.start.offset else selection.end.offset,
+            isStart = isStartHandle,
+            areHandlesCrossed = selection.handlesCrossed
+        )
+    }
+
+    override fun getLayoutCoordinates(): LayoutCoordinates? {
+        val layoutCoordinates = coordinatesCallback()
+        if (layoutCoordinates == null || !layoutCoordinates.isAttached) return null
+        return layoutCoordinates
+    }
+
+    override fun getText(): AnnotatedString {
+        val textLayoutResult = layoutResultCallback() ?: return AnnotatedString("")
+        return textLayoutResult.layoutInput.text
+    }
+
+    override fun getBoundingBox(offset: Int): Rect {
+        val textLayoutResult = layoutResultCallback() ?: return Rect.Zero
+        val textLength = textLayoutResult.layoutInput.text.length
+        if (textLength < 1) return Rect.Zero
+        return textLayoutResult.getBoundingBox(
+            offset.coerceIn(0, textLength - 1)
+        )
+    }
+
+    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))
+        return TextRange(
+            start = textLayoutResult.getLineStart(line),
+            end = textLayoutResult.getLineEnd(line, visibleEnd = true)
+        )
+    }
+}
+
+/**
+ * Return information about the current selection in the Text.
+ *
+ * @param textLayoutResult a result of the text layout.
+ * @param startHandlePosition The new positions of the moving selection handle.
+ * @param previousHandlePosition The old position of the moving selection handle since the last update.
+ * @param endHandlePosition the position of the selection handle that is not moving.
+ * @param selectableId the id of this [Selectable].
+ * @param adjustment the [SelectionAdjustment] used to process the raw selection range.
+ * @param previousSelection the previous text selection.
+ * @param isStartHandle whether the moving selection is the start selection handle.
+ *
+ * @return a pair consistent of updated [Selection] and a boolean representing whether the
+ * movement is consumed.
+ */
+internal fun getTextSelectionInfo(
+    textLayoutResult: TextLayoutResult,
+    startHandlePosition: Offset,
+    endHandlePosition: Offset,
+    previousHandlePosition: Offset?,
+    selectableId: Long,
+    adjustment: SelectionAdjustment,
+    previousSelection: Selection? = null,
+    isStartHandle: Boolean = true
+): Pair<Selection?, Boolean> {
+
+    val bounds = Rect(
+        0.0f,
+        0.0f,
+        textLayoutResult.size.width.toFloat(),
+        textLayoutResult.size.height.toFloat()
+    )
+
+    val isSelected =
+        SelectionMode.Vertical.isSelected(bounds, startHandlePosition, endHandlePosition)
+
+    if (!isSelected) {
+        return Pair(null, false)
+    }
+
+    val rawStartHandleOffset = getOffsetForPosition(textLayoutResult, bounds, startHandlePosition)
+    val rawEndHandleOffset = getOffsetForPosition(textLayoutResult, bounds, endHandlePosition)
+    val rawPreviousHandleOffset = previousHandlePosition?.let {
+        getOffsetForPosition(textLayoutResult, bounds, it)
+    } ?: -1
+
+    val adjustedTextRange = adjustment.adjust(
+        textLayoutResult = textLayoutResult,
+        newRawSelectionRange = TextRange(rawStartHandleOffset, rawEndHandleOffset),
+        previousHandleOffset = rawPreviousHandleOffset,
+        isStartHandle = isStartHandle,
+        previousSelectionRange = previousSelection?.toTextRange()
+    )
+    val newSelection = getAssembledSelectionInfo(
+        newSelectionRange = adjustedTextRange,
+        handlesCrossed = adjustedTextRange.reversed,
+        selectableId = selectableId,
+        textLayoutResult = textLayoutResult
+    )
+
+    // Determine whether the movement is consumed by this Selectable.
+    // If the selection has  changed, the movement is consumed.
+    // And there are also cases where the selection stays the same but selection handle raw
+    // offset has changed.(Usually this happen because of adjustment like SelectionAdjustment.Word)
+    // In this case we also consider the movement being consumed.
+    val selectionUpdated = newSelection != previousSelection
+    val handleUpdated = if (isStartHandle) {
+        rawStartHandleOffset != rawPreviousHandleOffset
+    } else {
+        rawEndHandleOffset != rawPreviousHandleOffset
+    }
+    val consumed = handleUpdated || selectionUpdated
+    return Pair(newSelection, consumed)
+}
+
+internal fun getOffsetForPosition(
+    textLayoutResult: TextLayoutResult,
+    bounds: Rect,
+    position: Offset
+): Int {
+    val length = textLayoutResult.layoutInput.text.length
+    return if (bounds.contains(position)) {
+        textLayoutResult.getOffsetForPosition(position).coerceIn(0, length)
+    } else {
+        val value = SelectionMode.Vertical.compare(position, bounds)
+        if (value < 0) 0 else length
+    }
+}
+
+/**
+ * [Selection] contains a lot of parameters. It looks more clean to assemble an object of this
+ * class in a separate method.
+ *
+ * @param newSelectionRange the final new selection text range.
+ * @param handlesCrossed true if the selection handles are crossed
+ * @param selectableId the id of the current [Selectable] for which the [Selection] is being
+ * calculated
+ * @param textLayoutResult a result of the text layout.
+ *
+ * @return an assembled object of [Selection] using the offered selection info.
+ */
+private fun getAssembledSelectionInfo(
+    newSelectionRange: TextRange,
+    handlesCrossed: Boolean,
+    selectableId: Long,
+    textLayoutResult: TextLayoutResult
+): Selection {
+    return Selection(
+        start = Selection.AnchorInfo(
+            direction = textLayoutResult.getBidiRunDirection(newSelectionRange.start),
+            offset = newSelectionRange.start,
+            selectableId = selectableId
+        ),
+        end = Selection.AnchorInfo(
+            direction = textLayoutResult.getBidiRunDirection(max(newSelectionRange.end - 1, 0)),
+            offset = newSelectionRange.end,
+            selectableId = selectableId
+        ),
+        handlesCrossed = handlesCrossed
+    )
+}
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/Selectable.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/Selectable.kt
new file mode 100644
index 0000000..c921a2d
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/Selectable.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.compose.foundation.newtext.text.copypasta.selection
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextRange
+
+/**
+ * Provides [Selection] information for a composable to SelectionContainer. Composables who can
+ * be selected should subscribe to [SelectionRegistrar] using this interface.
+ */
+
+internal interface Selectable {
+    /**
+     * An ID used by [SelectionRegistrar] to identify this [Selectable]. This value should not be
+     * [SelectionRegistrar.InvalidSelectableId].
+     * When a [Selectable] is created, it can request an ID from [SelectionRegistrar] by
+     * calling [SelectionRegistrar.nextSelectableId].
+     * @see SelectionRegistrar.nextSelectableId
+     */
+    val selectableId: Long
+
+    /**
+     * Updates the [Selection] information after a selection handle being moved. This method is
+     * expected to be called consecutively during the selection handle position update.
+     *
+     * @param startHandlePosition graphical position of the start selection handle
+     * @param endHandlePosition graphical position of the end selection handle
+     * @param previousHandlePosition the previous position of the moving selection handle
+     * @param containerLayoutCoordinates [LayoutCoordinates] of the composable
+     * @param adjustment [Selection] range is adjusted according to this param
+     * @param previousSelection previous selection result on this [Selectable]
+     * @param isStartHandle whether the moving selection handle is the start selection handle
+     *
+     * @throws IllegalStateException when the given [previousSelection] doesn't belong to this
+     * selectable. In other words, one of the [Selection.AnchorInfo] in the given
+     * [previousSelection] has a selectableId that doesn't match to the [selectableId] of this
+     * selectable.
+     * @return a pair consisting of the updated [Selection] and a boolean value representing
+     * whether the movement is consumed.
+     */
+    fun updateSelection(
+        startHandlePosition: Offset,
+        endHandlePosition: Offset,
+        previousHandlePosition: Offset?,
+        isStartHandle: Boolean = true,
+        containerLayoutCoordinates: LayoutCoordinates,
+        adjustment: SelectionAdjustment,
+        previousSelection: Selection? = null
+    ): Pair<Selection?, Boolean>
+
+    /**
+     * Returns selectAll [Selection] information for a selectable composable. If no selection can be
+     * provided null should be returned.
+     *
+     * @return selectAll [Selection] information for a selectable composable. If no selection can be
+     * provided null should be returned.
+     */
+    fun getSelectAllSelection(): Selection?
+
+    /**
+     * Return the [Offset] of a [SelectionHandle].
+     *
+     * @param selection [Selection] contains the [SelectionHandle]
+     * @param isStartHandle true if it's the start handle, false if it's the end handle.
+     *
+     * @return [Offset] of this handle, based on which the [SelectionHandle] will be drawn.
+     */
+    fun getHandlePosition(selection: Selection, isStartHandle: Boolean): Offset
+
+    /**
+     * Return the [LayoutCoordinates] of the [Selectable].
+     *
+     * @return [LayoutCoordinates] of the [Selectable]. This could be null if called before
+     * composing.
+     */
+    fun getLayoutCoordinates(): LayoutCoordinates?
+
+    /**
+     * Return the [AnnotatedString] of the [Selectable].
+     *
+     * @return text content as [AnnotatedString] of the [Selectable].
+     */
+    fun getText(): AnnotatedString
+
+    /**
+     * Return the bounding box of the character for given character offset. This is currently for
+     * text.
+     * In future when we implemented other selectable Composables, we can return the bounding box of
+     * the wanted rectangle. For example, for an image selectable, this should return the
+     * bounding box of the image.
+     *
+     * @param offset a character offset
+     * @return the bounding box for the character in [Rect], or [Rect.Zero] if the selectable is
+     * empty.
+     */
+    fun getBoundingBox(offset: Int): Rect
+
+    /**
+     * 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.
+     */
+    fun getRangeOfLineContaining(offset: Int): TextRange
+}
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/Selection.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/Selection.kt
new file mode 100644
index 0000000..5cedac2
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/Selection.kt
@@ -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.compose.foundation.newtext.text.copypasta.selection
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.style.ResolvedTextDirection
+
+/**
+ * Information about the current Selection.
+ */
+@Immutable
+internal data class Selection(
+    /**
+     * Information about the start of the selection.
+     */
+    val start: AnchorInfo,
+
+    /**
+     * Information about the end of the selection.
+     */
+    val end: AnchorInfo,
+    /**
+     * The flag to show that the selection handles are dragged across each other. After selection
+     * is initialized, if user drags one handle to cross the other handle, this is true, otherwise
+     * it's false.
+     */
+    // If selection happens in single widget, checking [TextRange.start] > [TextRange.end] is
+    // enough.
+    // But when selection happens across multiple widgets, this value needs more complicated
+    // calculation. To avoid repeated calculation, making it as a flag is cheaper.
+    val handlesCrossed: Boolean = false
+) {
+    /**
+     * Contains information about an anchor (start/end) of selection.
+     */
+    @Immutable
+    internal data class AnchorInfo(
+        /**
+         * Text direction of the character in selection edge.
+         */
+        val direction: ResolvedTextDirection,
+
+        /**
+         * Character offset for the selection edge. This offset is within individual child text
+         * composable.
+         */
+        val offset: Int,
+
+        /**
+         * The id of the [Selectable] which contains this [Selection] Anchor.
+         */
+        val selectableId: Long
+    )
+
+    fun merge(other: Selection?): Selection {
+        if (other == null) return this
+
+        var selection = this
+        selection = if (handlesCrossed) {
+            selection.copy(start = other.start)
+        } else {
+            selection.copy(end = other.end)
+        }
+
+        return selection
+    }
+
+    /**
+     * Returns the selection offset information as a [TextRange]
+     */
+    fun toTextRange(): TextRange {
+        return TextRange(start.offset, end.offset)
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionAdjustment.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionAdjustment.kt
new file mode 100644
index 0000000..c53cf0a
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionAdjustment.kt
@@ -0,0 +1,466 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.copypasta.selection
+
+import androidx.compose.foundation.newtext.text.copypasta.getParagraphBoundary
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
+
+/**
+ * Selection can be adjusted depends on context. For example, in touch mode dragging after a long
+ * press adjusts selection by word. But selection by dragging handles is character precise
+ * without adjustments. With a mouse, double-click selects by words and triple-clicks by paragraph.
+ * @see [SelectionRegistrar.notifySelectionUpdate]
+ */
+internal interface SelectionAdjustment {
+
+    /**
+     * The callback function that is called once a new selection arrives, the return value of
+     * this function will be the final selection range on the corresponding [Selectable].
+     *
+     * @param textLayoutResult the [TextLayoutResult] of the involved [Selectable].
+     * @param newRawSelectionRange the new selection range computed from the selection handle
+     * position on screen.
+     * @param previousHandleOffset the previous offset of the moving handle. When isStartHandle is
+     * true, it's the previous offset of the start handle before the movement, and vice versa.
+     * When there isn't a valid previousHandleOffset, previousHandleOffset should be -1.
+     * @param isStartHandle whether the moving handle is the start handle.
+     * @param previousSelectionRange the previous selection range, or the selection range to be
+     * updated.
+     */
+    fun adjust(
+        textLayoutResult: TextLayoutResult,
+        newRawSelectionRange: TextRange,
+        previousHandleOffset: Int,
+        isStartHandle: Boolean,
+        previousSelectionRange: TextRange?
+    ): TextRange
+
+    companion object {
+        /**
+         * The selection adjustment that does nothing and directly return the input raw
+         * selection range.
+         */
+        val None = object : SelectionAdjustment {
+            override fun adjust(
+                textLayoutResult: TextLayoutResult,
+                newRawSelectionRange: TextRange,
+                previousHandleOffset: Int,
+                isStartHandle: Boolean,
+                previousSelectionRange: TextRange?
+            ): TextRange = newRawSelectionRange
+        }
+
+        /**
+         * The character based selection. It normally won't change the raw selection range except
+         * when the input raw selection range is collapsed. In this case, it will always make
+         * sure at least one character is selected.
+         * When the given raw selection range is collapsed:
+         * a) it will always try to adjust the changing selection boundary(base on the value of
+         * isStartHandle) and makes sure the other boundary remains the same after the adjustment
+         * b) if the previous selection range is reversed, it will try to make the adjusted
+         * selection range reversed as well, and vice versa.
+         */
+        val Character = object : SelectionAdjustment {
+            override fun adjust(
+                textLayoutResult: TextLayoutResult,
+                newRawSelectionRange: TextRange,
+                previousHandleOffset: Int,
+                isStartHandle: Boolean,
+                previousSelectionRange: TextRange?
+            ): TextRange {
+                return if (newRawSelectionRange.collapsed) {
+                    // If there isn't any selection before, we assume handles are not crossed.
+                    val previousHandlesCrossed = previousSelectionRange?.reversed ?: false
+                    ensureAtLeastOneChar(
+                        offset = newRawSelectionRange.start,
+                        lastOffset = textLayoutResult.layoutInput.text.lastIndex,
+                        isStartHandle = isStartHandle,
+                        previousHandlesCrossed = previousHandlesCrossed
+                    )
+                } else {
+                    newRawSelectionRange
+                }
+            }
+        }
+
+        /**
+         * The word based selection adjustment. It will adjust the raw input selection such that
+         * the selection boundary snap to the word boundary. It will always expand the raw input
+         * selection range to the closest word boundary. If the raw selection is reversed, it
+         * will always return a reversed selection, and vice versa.
+         */
+        val Word = object : SelectionAdjustment {
+            override fun adjust(
+                textLayoutResult: TextLayoutResult,
+                newRawSelectionRange: TextRange,
+                previousHandleOffset: Int,
+                isStartHandle: Boolean,
+                previousSelectionRange: TextRange?
+            ): TextRange {
+                return adjustByBoundary(
+                    textLayoutResult = textLayoutResult,
+                    newRawSelection = newRawSelectionRange,
+                    boundaryFun = textLayoutResult::getWordBoundary
+                )
+            }
+        }
+
+        /**
+         * The paragraph based selection adjustment. It will adjust the raw input selection such
+         * that the selection boundary snap to the paragraph boundary. It will always expand the
+         * raw input selection range to the closest paragraph boundary. If the raw selection is
+         * reversed, it will always return a reversed selection, and vice versa.
+         */
+        val Paragraph = object : SelectionAdjustment {
+            override fun adjust(
+                textLayoutResult: TextLayoutResult,
+                newRawSelectionRange: TextRange,
+                previousHandleOffset: Int,
+                isStartHandle: Boolean,
+                previousSelectionRange: TextRange?
+            ): TextRange {
+                val boundaryFun = textLayoutResult.layoutInput.text::getParagraphBoundary
+                return adjustByBoundary(
+                    textLayoutResult = textLayoutResult,
+                    newRawSelection = newRawSelectionRange,
+                    boundaryFun = boundaryFun
+                )
+            }
+        }
+
+        private fun adjustByBoundary(
+            textLayoutResult: TextLayoutResult,
+            newRawSelection: TextRange,
+            boundaryFun: (Int) -> TextRange
+        ): TextRange {
+            if (textLayoutResult.layoutInput.text.isEmpty()) {
+                return TextRange.Zero
+            }
+            val maxOffset = textLayoutResult.layoutInput.text.lastIndex
+            val startBoundary = boundaryFun(newRawSelection.start.coerceIn(0, maxOffset))
+            val endBoundary = boundaryFun(newRawSelection.end.coerceIn(0, maxOffset))
+
+            // If handles are not crossed, start should be snapped to the start of the word
+            // containing the start offset, and end should be snapped to the end of the word
+            // containing the end offset. If handles are crossed, start should be snapped to the
+            // end of the word containing the start offset, and end should be snapped to the start
+            // of the word containing the end offset.
+            val start = if (newRawSelection.reversed) startBoundary.end else startBoundary.start
+            val end = if (newRawSelection.reversed) endBoundary.start else endBoundary.end
+            return TextRange(start, end)
+        }
+
+        /**
+         * A special version of character based selection that accelerates the selection update
+         * with word based selection. In short, it expands by word and shrinks by character.
+         * Here is more details of the behavior:
+         * 1. When previous selection is null, it will use word based selection.
+         * 2. When the start/end offset has moved to a different line, it will use word
+         * based selection.
+         * 3. When the selection is shrinking, it behave same as the character based selection.
+         * Shrinking means that the start/end offset is moving in the direction that makes
+         * selected text shorter.
+         * 4. The selection boundary is expanding,
+         *  a.if the previous start/end offset is not a word boundary, use character based
+         * selection.
+         *  b.if the previous start/end offset is a word boundary, use word based selection.
+         *
+         *  Notice that this selection adjustment assumes that when isStartHandle is true, only
+         *  start handle is moving(or unchanged), and vice versa.
+         */
+        val CharacterWithWordAccelerate = object : SelectionAdjustment {
+            override fun adjust(
+                textLayoutResult: TextLayoutResult,
+                newRawSelectionRange: TextRange,
+                previousHandleOffset: Int,
+                isStartHandle: Boolean,
+                previousSelectionRange: TextRange?
+            ): TextRange {
+                // Previous selection is null. We start a word based selection.
+                if (previousSelectionRange == null) {
+                    return Word.adjust(
+                        textLayoutResult = textLayoutResult,
+                        newRawSelectionRange = newRawSelectionRange,
+                        previousHandleOffset = previousHandleOffset,
+                        isStartHandle = isStartHandle,
+                        previousSelectionRange = previousSelectionRange
+                    )
+                }
+
+                // The new selection is collapsed, ensure at least one char is selected.
+                if (newRawSelectionRange.collapsed) {
+                    return ensureAtLeastOneChar(
+                        offset = newRawSelectionRange.start,
+                        lastOffset = textLayoutResult.layoutInput.text.lastIndex,
+                        isStartHandle = isStartHandle,
+                        previousHandlesCrossed = previousSelectionRange.reversed
+                    )
+                }
+
+                val start: Int
+                val end: Int
+                if (isStartHandle) {
+                    start = updateSelectionBoundary(
+                        textLayoutResult = textLayoutResult,
+                        newRawOffset = newRawSelectionRange.start,
+                        previousRawOffset = previousHandleOffset,
+                        previousAdjustedOffset = previousSelectionRange.start,
+                        otherBoundaryOffset = newRawSelectionRange.end,
+                        isStart = true,
+                        isReversed = newRawSelectionRange.reversed
+                    )
+                    end = newRawSelectionRange.end
+                } else {
+                    start = newRawSelectionRange.start
+                    end = updateSelectionBoundary(
+                        textLayoutResult = textLayoutResult,
+                        newRawOffset = newRawSelectionRange.end,
+                        previousRawOffset = previousHandleOffset,
+                        previousAdjustedOffset = previousSelectionRange.end,
+                        otherBoundaryOffset = newRawSelectionRange.start,
+                        isStart = false,
+                        isReversed = newRawSelectionRange.reversed
+                    )
+                }
+                return TextRange(start, end)
+            }
+
+            /**
+             * Helper function that updates start or end boundary of the selection. It implements
+             * the "expand by word and shrink by character behavior".
+             *
+             * @param textLayoutResult the text layout result
+             * @param newRawOffset the new raw offset of the selection boundary after the movement.
+             * @param previousRawOffset the raw offset of the updated selection boundary before the
+             * movement. In the case where previousRawOffset invalid(when selection update is
+             * triggered by long-press or click) pass -1 for this parameter.
+             * @param previousAdjustedOffset the previous final/adjusted offset. It's the current
+             * @param otherBoundaryOffset the offset of the other selection boundary. It is used
+             * to avoid empty selection in word based selection mode.
+             * selection boundary.
+             * @param isStart whether it's updating the selection start or end boundary.
+             * @param isReversed whether the selection is reversed or not. We use
+             * this information to determine if the selection is expanding or shrinking.
+             */
+            private fun updateSelectionBoundary(
+                textLayoutResult: TextLayoutResult,
+                newRawOffset: Int,
+                previousRawOffset: Int,
+                previousAdjustedOffset: Int,
+                otherBoundaryOffset: Int,
+                isStart: Boolean,
+                isReversed: Boolean
+            ): Int {
+                // The raw offset didn't change, directly return the previous adjusted start offset.
+                if (newRawOffset == previousRawOffset) {
+                    return previousAdjustedOffset
+                }
+
+                val currentLine = textLayoutResult.getLineForOffset(newRawOffset)
+                val previousLine = textLayoutResult.getLineForOffset(previousAdjustedOffset)
+
+                // The updating selection boundary has crossed a line, use word based selection.
+                if (currentLine != previousLine) {
+                    return snapToWordBoundary(
+                        textLayoutResult = textLayoutResult,
+                        newRawOffset = newRawOffset,
+                        currentLine = currentLine,
+                        otherBoundaryOffset = otherBoundaryOffset,
+                        isStart = isStart,
+                        isReversed = isReversed
+                    )
+                }
+
+                // Check if the start or end selection boundary is expanding. If it's shrinking,
+                // use character based selection.
+                val isExpanding =
+                    isExpanding(newRawOffset, previousRawOffset, isStart, isReversed)
+                if (!isExpanding) {
+                    return newRawOffset
+                }
+
+                // If the previous start/end offset is not at a word boundary, which is indicating
+                // that start/end offset is updating within a word. In this case, it still uses
+                // character based selection.
+                if (!textLayoutResult.isAtWordBoundary(previousAdjustedOffset)) {
+                    return newRawOffset
+                }
+
+                // At this point we know, the updating start/end offset is still in the same line,
+                // it's expanding the selection, and it's not updating within a word. It should
+                // use word based selection.
+                return snapToWordBoundary(
+                    textLayoutResult = textLayoutResult,
+                    newRawOffset = newRawOffset,
+                    currentLine = currentLine,
+                    otherBoundaryOffset = otherBoundaryOffset,
+                    isStart = isStart,
+                    isReversed = isReversed
+                )
+            }
+
+            private fun snapToWordBoundary(
+                textLayoutResult: TextLayoutResult,
+                newRawOffset: Int,
+                currentLine: Int,
+                otherBoundaryOffset: Int,
+                isStart: Boolean,
+                isReversed: Boolean
+            ): Int {
+                val wordBoundary = textLayoutResult.getWordBoundary(newRawOffset)
+
+                // In the case where the target word crosses multiple lines due to hyphenation or
+                // being too long, we use the line start/end to keep the adjusted offset at the
+                // same line.
+                val wordStartLine = textLayoutResult.getLineForOffset(wordBoundary.start)
+                val start = if (wordStartLine == currentLine) {
+                    wordBoundary.start
+                } else {
+                    textLayoutResult.getLineStart(currentLine)
+                }
+
+                val wordEndLine = textLayoutResult.getLineForOffset(wordBoundary.end)
+                val end = if (wordEndLine == currentLine) {
+                    wordBoundary.end
+                } else {
+                    textLayoutResult.getLineEnd(currentLine)
+                }
+
+                // If one of the word boundary is exactly same as the otherBoundaryOffset, we
+                // can't snap to this word boundary since it will result in an empty selection
+                // range.
+                if (start == otherBoundaryOffset) {
+                    return end
+                }
+                if (end == otherBoundaryOffset) {
+                    return start
+                }
+
+                val threshold = (start + end) / 2
+                return if (isStart xor isReversed) {
+                    // In this branch when:
+                    // 1. selection is updating the start offset, and selection is not reversed.
+                    // 2. selection is updating the end offset, and selection is reversed.
+                    if (newRawOffset <= threshold) {
+                        start
+                    } else {
+                        end
+                    }
+                } else {
+                    // In this branch when:
+                    // 1. selection is updating the end offset, and selection is not reversed.
+                    // 2. selection is updating the start offset, and selection is reversed.
+                    if (newRawOffset >= threshold) {
+                        end
+                    } else {
+                        start
+                    }
+                }
+            }
+
+            private fun TextLayoutResult.isAtWordBoundary(offset: Int): Boolean {
+                val wordBoundary = getWordBoundary(offset)
+                return offset == wordBoundary.start || offset == wordBoundary.end
+            }
+
+            private fun isExpanding(
+                newRawOffset: Int,
+                previousRawOffset: Int,
+                isStart: Boolean,
+                previousReversed: Boolean
+            ): Boolean {
+                // -1 is considered as no previous offset, so the selection is expanding.
+                if (previousRawOffset == -1) {
+                    return true
+                }
+                if (newRawOffset == previousRawOffset) {
+                    return false
+                }
+                return if (isStart xor previousReversed) {
+                    newRawOffset < previousRawOffset
+                } else {
+                    newRawOffset > previousRawOffset
+                }
+            }
+        }
+    }
+}
+
+/**
+ * This method adjusts the raw start and end offset and bounds the selection to one character. The
+ * logic of bounding evaluates the last selection result, which handle is being dragged, and if
+ * selection reaches the boundary.
+ *
+ * @param offset unprocessed start and end offset calculated directly from input position, in
+ * this case start and offset equals to each other.
+ * @param lastOffset last offset of the text. It's actually the length of the text.
+ * @param isStartHandle true if the start handle is being dragged
+ * @param previousHandlesCrossed true if the selection handles are crossed in the previous
+ * selection. This function will try to maintain the handle cross state. This can help make
+ * selection stable.
+ *
+ * @return the adjusted [TextRange].
+ */
+internal fun ensureAtLeastOneChar(
+    offset: Int,
+    lastOffset: Int,
+    isStartHandle: Boolean,
+    previousHandlesCrossed: Boolean
+): TextRange {
+    // When lastOffset is 0, it can only return an empty TextRange.
+    // When previousSelection is null, it won't start a selection and return an empty TextRange.
+    if (lastOffset == 0) return TextRange(offset, offset)
+
+    // When offset is at the boundary, the handle that is not dragged should be at [offset]. Here
+    // the other handle's position is computed accordingly.
+    if (offset == 0) {
+        return if (isStartHandle) {
+            TextRange(1, 0)
+        } else {
+            TextRange(0, 1)
+        }
+    }
+
+    if (offset == lastOffset) {
+        return if (isStartHandle) {
+            TextRange(lastOffset - 1, lastOffset)
+        } else {
+            TextRange(lastOffset, lastOffset - 1)
+        }
+    }
+
+    // In other cases, this function will try to maintain the current cross handle states.
+    // Only in this way the selection can be stable.
+    return if (isStartHandle) {
+        if (!previousHandlesCrossed) {
+            // Handle is NOT crossed, and the start handle is dragged.
+            TextRange(offset - 1, offset)
+        } else {
+            // Handle is crossed, and the start handle is dragged.
+            TextRange(offset + 1, offset)
+        }
+    } else {
+        if (!previousHandlesCrossed) {
+            // Handle is NOT crossed, and the end handle is dragged.
+            TextRange(offset, offset + 1)
+        } else {
+            // Handle is crossed, and the end handle is dragged.
+            TextRange(offset, offset - 1)
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionContainer.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionContainer.kt
new file mode 100644
index 0000000..82d53aa
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionContainer.kt
@@ -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.compose.foundation.newtext.text.copypasta.selection
+
+import androidx.compose.foundation.newtext.text.copypasta.ContextMenuArea
+import androidx.compose.foundation.newtext.text.copypasta.detectDownAndDragGesturesWithObserver
+import androidx.compose.foundation.newtext.text.copypasta.isInTouchMode
+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.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.platform.LocalTextToolbar
+import androidx.compose.ui.util.fastForEach
+
+/**
+ * Enables text selection for its direct or indirect children.
+ *
+ * @sample androidx.compose.foundation.samples.SelectionSample
+ */
+@Composable
+fun SelectionContainer(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+    var selection by remember { mutableStateOf<Selection?>(null) }
+    SelectionContainer(
+        modifier = modifier,
+        selection = selection,
+        onSelectionChange = {
+            selection = it
+        },
+        children = content
+    )
+}
+
+/**
+ * Disables text selection for its direct or indirect children. To use this, simply add this
+ * to wrap one or more text composables.
+ *
+ * @sample androidx.compose.foundation.samples.DisableSelectionSample
+ */
+@Composable
+fun DisableSelection(content: @Composable () -> Unit) {
+    CompositionLocalProvider(
+        LocalSelectionRegistrar provides null,
+        content = content
+    )
+}
+
+/**
+ * Selection Composable.
+ *
+ * The selection composable wraps composables and let them to be selectable. It paints the selection
+ * area with start and end handles.
+ */
+@Suppress("ComposableLambdaParameterNaming")
+@Composable
+internal fun SelectionContainer(
+    /** A [Modifier] for SelectionContainer. */
+    modifier: Modifier = Modifier,
+    /** Current Selection status.*/
+    selection: Selection?,
+    /** A function containing customized behaviour when selection changes. */
+    onSelectionChange: (Selection?) -> Unit,
+    children: @Composable () -> Unit
+) {
+    val registrarImpl = remember { SelectionRegistrarImpl() }
+    val manager = remember { SelectionManager(registrarImpl) }
+
+    manager.hapticFeedBack = LocalHapticFeedback.current
+    manager.clipboardManager = LocalClipboardManager.current
+    manager.textToolbar = LocalTextToolbar.current
+    manager.onSelectionChange = onSelectionChange
+    manager.selection = selection
+    manager.touchMode = isInTouchMode
+
+    ContextMenuArea(manager) {
+        CompositionLocalProvider(LocalSelectionRegistrar provides registrarImpl) {
+            // Get the layout coordinates of the selection container. This is for hit test of
+            // cross-composable selection.
+            SimpleLayout(modifier = modifier.then(manager.modifier)) {
+                children()
+                if (isInTouchMode && manager.hasFocus) {
+                    manager.selection?.let {
+                        listOf(true, false).fastForEach { isStartHandle ->
+                            val observer = remember(isStartHandle) {
+                                manager.handleDragObserver(isStartHandle)
+                            }
+                            val position = if (isStartHandle) {
+                                manager.startHandlePosition
+                            } else {
+                                manager.endHandlePosition
+                            }
+
+                            val direction = if (isStartHandle) {
+                                it.start.direction
+                            } else {
+                                it.end.direction
+                            }
+
+                            if (position != null) {
+                                SelectionHandle(
+                                    position = position,
+                                    isStartHandle = isStartHandle,
+                                    direction = direction,
+                                    handlesCrossed = it.handlesCrossed,
+                                    modifier = Modifier.pointerInput(observer) {
+                                        detectDownAndDragGesturesWithObserver(observer)
+                                    },
+                                    content = null
+                                )
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    DisposableEffect(manager) {
+        onDispose {
+            manager.hideSelectionToolbar()
+        }
+    }
+}
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionHandles.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionHandles.kt
new file mode 100644
index 0000000..ddea64a
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionHandles.kt
@@ -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.compose.foundation.newtext.text.copypasta.selection
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.semantics.SemanticsPropertyKey
+import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.unit.dp
+
+internal val HandleWidth = 25.dp
+internal val HandleHeight = 25.dp
+
+/**
+ * [SelectionHandleInfo]s for the nodes representing selection handles. These nodes are in popup
+ * windows, and will respond to drag gestures.
+ */
+internal val SelectionHandleInfoKey =
+    SemanticsPropertyKey<SelectionHandleInfo>("SelectionHandleInfo")
+
+/**
+ * Information about a single selection handle popup.
+ *
+ * @param position The position that the handle is anchored to relative to the selectable content.
+ * This position is not necessarily the position of the popup itself, it's the position that the
+ * handle "points" to
+ */
+internal data class SelectionHandleInfo(
+    // TODO: This is removed for copypasta because it's never used in basic usage
+    // val handle: Handle,
+    val position: Offset
+)
+
+@Composable
+internal expect fun SelectionHandle(
+    position: Offset,
+    isStartHandle: Boolean,
+    direction: ResolvedTextDirection,
+    handlesCrossed: Boolean,
+    modifier: Modifier,
+    content: @Composable (() -> Unit)?
+)
+
+/**
+ * Adjust coordinates for given text offset.
+ *
+ * Currently [android.text.Layout.getLineBottom] returns y coordinates of the next
+ * line's top offset, which is not included in current line's hit area. To be able to
+ * hit current line, move up this y coordinates by 1 pixel.
+ */
+internal fun getAdjustedCoordinates(position: Offset): Offset {
+    return Offset(position.x, position.y - 1f)
+}
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionMagnifier.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionMagnifier.kt
new file mode 100644
index 0000000..2817f3e
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionMagnifier.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.copypasta.selection
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector2D
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.TwoWayConverter
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isSpecified
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+private val UnspecifiedAnimationVector2D = AnimationVector2D(Float.NaN, Float.NaN)
+
+/** Like `Offset.VectorConverter` but propagates [Offset.Unspecified] values. */
+private val UnspecifiedSafeOffsetVectorConverter = TwoWayConverter<Offset, AnimationVector2D>(
+    convertToVector = {
+        if (it.isSpecified) {
+            AnimationVector2D(it.x, it.y)
+        } else {
+            UnspecifiedAnimationVector2D
+        }
+    },
+    convertFromVector = { Offset(it.v1, it.v2) }
+)
+
+private val OffsetDisplacementThreshold = Offset(
+    Spring.DefaultDisplacementThreshold,
+    Spring.DefaultDisplacementThreshold
+)
+
+private val MagnifierSpringSpec = SpringSpec(visibilityThreshold = OffsetDisplacementThreshold)
+
+/**
+ * The text magnifier follows horizontal dragging exactly, but is vertically clamped to the current
+ * line, so when it changes lines we animate it.
+ */
+@Suppress("ModifierInspectorInfo")
+internal fun Modifier.animatedSelectionMagnifier(
+    magnifierCenter: () -> Offset,
+    platformMagnifier: (animatedCenter: () -> Offset) -> Modifier
+): Modifier = composed {
+    val animatedCenter by rememberAnimatedMagnifierPosition(targetCalculation = magnifierCenter)
+    return@composed platformMagnifier { animatedCenter }
+}
+
+/**
+ * Remembers and returns a [State] that will smoothly animate to the result of [targetCalculation]
+ * any time the result of [targetCalculation] changes due to any state values it reads change.
+ */
+@Composable
+private fun rememberAnimatedMagnifierPosition(
+    targetCalculation: () -> Offset,
+): State<Offset> {
+    val targetValue by remember { derivedStateOf(targetCalculation) }
+    val animatable = remember {
+        // Can't use Offset.VectorConverter because we need to handle Unspecified specially.
+        Animatable(targetValue, UnspecifiedSafeOffsetVectorConverter, OffsetDisplacementThreshold)
+    }
+    LaunchedEffect(Unit) {
+        val animationScope = this
+        snapshotFlow { targetValue }
+            .collect { targetValue ->
+                // Only animate the position when moving vertically (i.e. jumping between lines),
+                // since horizontal movement in a single line should stay as close to the gesture as
+                // possible and animation would only add unnecessary lag.
+                if (
+                    animatable.value.isSpecified &&
+                    targetValue.isSpecified &&
+                    animatable.value.y != targetValue.y
+                ) {
+                    // Launch the animation, instead of cancelling and re-starting manually via
+                    // collectLatest, so if another animation is started before this one finishes,
+                    // the new one will use the correct velocity, e.g. in order to propagate spring
+                    // inertia.
+                    animationScope.launch {
+                        animatable.animateTo(targetValue, MagnifierSpringSpec)
+                    }
+                } else {
+                    animatable.snapTo(targetValue)
+                }
+            }
+    }
+    return animatable.asState()
+}
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionManager.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionManager.kt
new file mode 100644
index 0000000..73e0612
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionManager.kt
@@ -0,0 +1,903 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.copypasta.selection
+
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.waitForUpOrCancellation
+import androidx.compose.foundation.newtext.text.copypasta.TextDragObserver
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.onFocusChanged
+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.KeyEvent
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.boundsInWindow
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInWindow
+import androidx.compose.ui.platform.ClipboardManager
+import androidx.compose.ui.platform.TextToolbar
+import androidx.compose.ui.platform.TextToolbarStatus
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.unit.IntSize
+import kotlin.math.absoluteValue
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * A bridge class between user interaction to the text composables for text selection.
+ */
+internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
+
+    private val _selection: MutableState<Selection?> = mutableStateOf(null)
+
+    /**
+     * The current selection.
+     */
+    var selection: Selection?
+        get() = _selection.value
+        set(value) {
+            _selection.value = value
+            if (value != null) {
+                updateHandleOffsets()
+            }
+        }
+
+    /**
+     * Is touch mode active
+     */
+    var touchMode: Boolean = true
+
+    /**
+     * The manager will invoke this every time it comes to the conclusion that the selection should
+     * change. The expectation is that this callback will end up causing `setSelection` to get
+     * called. This is what makes this a "controlled component".
+     */
+    var onSelectionChange: (Selection?) -> Unit = {}
+
+    /**
+     * [HapticFeedback] handle to perform haptic feedback.
+     */
+    var hapticFeedBack: HapticFeedback? = null
+
+    /**
+     * [ClipboardManager] to perform clipboard features.
+     */
+    var clipboardManager: ClipboardManager? = null
+
+    /**
+     * [TextToolbar] to show floating toolbar(post-M) or primary toolbar(pre-M).
+     */
+    var textToolbar: TextToolbar? = null
+
+    /**
+     * Focus requester used to request focus when selection becomes active.
+     */
+    var focusRequester: FocusRequester = FocusRequester()
+
+    /**
+     * Return true if the corresponding SelectionContainer is focused.
+     */
+    var hasFocus: Boolean by mutableStateOf(false)
+
+    /**
+     * Modifier for selection container.
+     */
+    val modifier
+        get() = Modifier
+            .onClearSelectionRequested { onRelease() }
+            .onGloballyPositioned { containerLayoutCoordinates = it }
+            .focusRequester(focusRequester)
+            .onFocusChanged { focusState ->
+                if (!focusState.isFocused && hasFocus) {
+                    onRelease()
+                }
+                hasFocus = focusState.isFocused
+            }
+            .focusable()
+            .onKeyEvent {
+                if (isCopyKeyEvent(it)) {
+                    copy()
+                    true
+                } else {
+                    false
+                }
+            }
+            .then(if (shouldShowMagnifier) Modifier.selectionMagnifier(this) else Modifier)
+
+    private var previousPosition: Offset? = null
+
+    /**
+     * Layout Coordinates of the selection container.
+     */
+    var containerLayoutCoordinates: LayoutCoordinates? = null
+        set(value) {
+            field = value
+            if (hasFocus && selection != null) {
+                val positionInWindow = value?.positionInWindow()
+                if (previousPosition != positionInWindow) {
+                    previousPosition = positionInWindow
+                    updateHandleOffsets()
+                    updateSelectionToolbarPosition()
+                }
+            }
+        }
+
+    /**
+     * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
+     * recalculated.
+     */
+    internal var dragBeginPosition by mutableStateOf(Offset.Zero)
+        private set
+
+    /**
+     * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
+     * it will be zeroed out.
+     */
+    internal var dragTotalDistance by mutableStateOf(Offset.Zero)
+        private set
+
+    /**
+     * The calculated position of the start handle in the [SelectionContainer] coordinates. It
+     * is null when handle shouldn't be displayed.
+     * It is a [State] so reading it during the composition will cause recomposition every time
+     * the position has been changed.
+     */
+    var startHandlePosition: Offset? by mutableStateOf(null)
+        private set
+
+    /**
+     * The calculated position of the end handle in the [SelectionContainer] coordinates. It
+     * is null when handle shouldn't be displayed.
+     * It is a [State] so reading it during the composition will cause recomposition every time
+     * the position has been changed.
+     */
+    var endHandlePosition: Offset? by mutableStateOf(null)
+        private set
+
+    /**
+     * The handle that is currently being dragged, or null when no handle is being dragged. To get
+     * the position of the last drag event, use [currentDragPosition].
+     */
+    var draggingHandle: Handle? by mutableStateOf(null)
+        private set
+
+    /**
+     * When a handle is being dragged (i.e. [draggingHandle] is non-null), this is the last position
+     * of the actual drag event. It is not clamped to handle positions. Null when not being dragged.
+     */
+    var currentDragPosition: Offset? by mutableStateOf(null)
+        private set
+
+    private val shouldShowMagnifier get() = draggingHandle != null
+
+    init {
+        selectionRegistrar.onPositionChangeCallback = { selectableId ->
+            if (
+                selectableId == selection?.start?.selectableId ||
+                selectableId == selection?.end?.selectableId
+            ) {
+                updateHandleOffsets()
+                updateSelectionToolbarPosition()
+            }
+        }
+
+        selectionRegistrar.onSelectionUpdateStartCallback =
+            { layoutCoordinates, position, selectionMode ->
+                val positionInContainer = convertToContainerCoordinates(
+                    layoutCoordinates,
+                    position
+                )
+
+                if (positionInContainer != null) {
+                    startSelection(
+                        position = positionInContainer,
+                        isStartHandle = false,
+                        adjustment = selectionMode
+                    )
+
+                    focusRequester.requestFocus()
+                    hideSelectionToolbar()
+                }
+            }
+
+        selectionRegistrar.onSelectionUpdateSelectAll =
+            { selectableId ->
+                val (newSelection, newSubselection) = selectAll(
+                    selectableId = selectableId,
+                    previousSelection = selection,
+                )
+                if (newSelection != selection) {
+                    selectionRegistrar.subselections = newSubselection
+                    onSelectionChange(newSelection)
+                }
+
+                focusRequester.requestFocus()
+                hideSelectionToolbar()
+            }
+
+        selectionRegistrar.onSelectionUpdateCallback =
+            { layoutCoordinates, newPosition, previousPosition, isStartHandle, selectionMode ->
+                val newPositionInContainer =
+                    convertToContainerCoordinates(layoutCoordinates, newPosition)
+                val previousPositionInContainer =
+                    convertToContainerCoordinates(layoutCoordinates, previousPosition)
+
+                updateSelection(
+                    newPosition = newPositionInContainer,
+                    previousPosition = previousPositionInContainer,
+                    isStartHandle = isStartHandle,
+                    adjustment = selectionMode
+                )
+            }
+
+        selectionRegistrar.onSelectionUpdateEndCallback = {
+            showSelectionToolbar()
+            // This property is set by updateSelection while dragging, so we need to clear it after
+            // the original selection drag.
+            draggingHandle = null
+            currentDragPosition = null
+        }
+
+        selectionRegistrar.onSelectableChangeCallback = { selectableKey ->
+            if (selectableKey in selectionRegistrar.subselections) {
+                // clear the selection range of each Selectable.
+                onRelease()
+                selection = null
+            }
+        }
+
+        selectionRegistrar.afterSelectableUnsubscribe = { selectableKey ->
+            if (
+                selectableKey == selection?.start?.selectableId ||
+                selectableKey == selection?.end?.selectableId
+            ) {
+                // The selectable that contains a selection handle just unsubscribed.
+                // Hide selection handles for now
+                startHandlePosition = null
+                endHandlePosition = null
+            }
+        }
+    }
+
+    /**
+     * Returns the [Selectable] responsible for managing the given [Selection.AnchorInfo], or null
+     * if the anchor is not from a currently-registered [Selectable].
+     */
+    internal fun getAnchorSelectable(anchor: Selection.AnchorInfo): Selectable? {
+        return selectionRegistrar.selectableMap[anchor.selectableId]
+    }
+
+    private fun updateHandleOffsets() {
+        val selection = selection
+        val containerCoordinates = containerLayoutCoordinates
+        val startSelectable = selection?.start?.let(::getAnchorSelectable)
+        val endSelectable = selection?.end?.let(::getAnchorSelectable)
+        val startLayoutCoordinates = startSelectable?.getLayoutCoordinates()
+        val endLayoutCoordinates = endSelectable?.getLayoutCoordinates()
+        if (
+            selection == null ||
+            containerCoordinates == null ||
+            !containerCoordinates.isAttached ||
+            startLayoutCoordinates == null ||
+            endLayoutCoordinates == null
+        ) {
+            this.startHandlePosition = null
+            this.endHandlePosition = null
+            return
+        }
+
+        val startHandlePosition = containerCoordinates.localPositionOf(
+            startLayoutCoordinates,
+            startSelectable.getHandlePosition(
+                selection = selection,
+                isStartHandle = true
+            )
+        )
+        val endHandlePosition = containerCoordinates.localPositionOf(
+            endLayoutCoordinates,
+            endSelectable.getHandlePosition(
+                selection = selection,
+                isStartHandle = false
+            )
+        )
+
+        val visibleBounds = containerCoordinates.visibleBounds()
+        this.startHandlePosition =
+            if (visibleBounds.containsInclusive(startHandlePosition)) startHandlePosition else null
+        this.endHandlePosition =
+            if (visibleBounds.containsInclusive(endHandlePosition)) endHandlePosition else null
+    }
+
+    /**
+     * Returns non-nullable [containerLayoutCoordinates].
+     */
+    internal fun requireContainerCoordinates(): LayoutCoordinates {
+        val coordinates = containerLayoutCoordinates
+        require(coordinates != null)
+        require(coordinates.isAttached)
+        return coordinates
+    }
+
+    internal fun selectAll(
+        selectableId: Long,
+        previousSelection: Selection?
+    ): Pair<Selection?, Map<Long, Selection>> {
+        val subselections = mutableMapOf<Long, Selection>()
+        val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
+            .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
+                val selection = if (selectable.selectableId == selectableId)
+                    selectable.getSelectAllSelection() else null
+                selection?.let { subselections[selectable.selectableId] = it }
+                merge(mergedSelection, selection)
+            }
+        if (newSelection != previousSelection) {
+            hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
+        }
+        return Pair(newSelection, subselections)
+    }
+
+    internal fun getSelectedText(): AnnotatedString? {
+        val selectables = selectionRegistrar.sort(requireContainerCoordinates())
+        var selectedText: AnnotatedString? = null
+
+        selection?.let {
+            for (i in selectables.indices) {
+                val selectable = selectables[i]
+                // Continue if the current selectable is before the selection starts.
+                if (selectable.selectableId != it.start.selectableId &&
+                    selectable.selectableId != it.end.selectableId &&
+                    selectedText == null
+                ) continue
+
+                val currentSelectedText = getCurrentSelectedText(
+                    selectable = selectable,
+                    selection = it
+                )
+                selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
+
+                // Break if the current selectable is the last selected selectable.
+                if (selectable.selectableId == it.end.selectableId && !it.handlesCrossed ||
+                    selectable.selectableId == it.start.selectableId && it.handlesCrossed
+                ) break
+            }
+        }
+        return selectedText
+    }
+
+    internal fun copy() {
+        val selectedText = getSelectedText()
+        selectedText?.let { clipboardManager?.setText(it) }
+    }
+
+    /**
+     * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
+     * to make the FloatingToolbar show up in the proper place. In addition, this function passes
+     * the copy method as a callback when "copy" is clicked.
+     */
+    internal fun showSelectionToolbar() {
+        if (hasFocus) {
+            selection?.let {
+                textToolbar?.showMenu(
+                    getContentRect(),
+                    onCopyRequested = {
+                        copy()
+                        onRelease()
+                    }
+                )
+            }
+        }
+    }
+
+    internal fun hideSelectionToolbar() {
+        if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
+            textToolbar?.hide()
+        }
+    }
+
+    private fun updateSelectionToolbarPosition() {
+        if (hasFocus && textToolbar?.status == TextToolbarStatus.Shown) {
+            showSelectionToolbar()
+        }
+    }
+
+    /**
+     * Calculate selected region as [Rect]. The top is the top of the first selected
+     * line, and the bottom is the bottom of the last selected line. The left is the leftmost
+     * handle's horizontal coordinates, and the right is the rightmost handle's coordinates.
+     */
+    private fun getContentRect(): Rect {
+        val selection = selection ?: return Rect.Zero
+        val startSelectable = getAnchorSelectable(selection.start)
+        val endSelectable = getAnchorSelectable(selection.end)
+        val startLayoutCoordinates = startSelectable?.getLayoutCoordinates() ?: return Rect.Zero
+        val endLayoutCoordinates = endSelectable?.getLayoutCoordinates() ?: return Rect.Zero
+
+        val localLayoutCoordinates = containerLayoutCoordinates
+        if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
+            var startOffset = localLayoutCoordinates.localPositionOf(
+                startLayoutCoordinates,
+                startSelectable.getHandlePosition(
+                    selection = selection,
+                    isStartHandle = true
+                )
+            )
+            var endOffset = localLayoutCoordinates.localPositionOf(
+                endLayoutCoordinates,
+                endSelectable.getHandlePosition(
+                    selection = selection,
+                    isStartHandle = false
+                )
+            )
+
+            startOffset = localLayoutCoordinates.localToRoot(startOffset)
+            endOffset = localLayoutCoordinates.localToRoot(endOffset)
+
+            val left = min(startOffset.x, endOffset.x)
+            val right = max(startOffset.x, endOffset.x)
+
+            var startTop = localLayoutCoordinates.localPositionOf(
+                startLayoutCoordinates,
+                Offset(
+                    0f,
+                    startSelectable.getBoundingBox(selection.start.offset).top
+                )
+            )
+
+            var endTop = localLayoutCoordinates.localPositionOf(
+                endLayoutCoordinates,
+                Offset(
+                    0.0f,
+                    endSelectable.getBoundingBox(selection.end.offset).top
+                )
+            )
+
+            startTop = localLayoutCoordinates.localToRoot(startTop)
+            endTop = localLayoutCoordinates.localToRoot(endTop)
+
+            val top = min(startTop.y, endTop.y)
+            val bottom = max(startOffset.y, endOffset.y) + (HandleHeight.value * 4.0).toFloat()
+
+            return Rect(
+                left,
+                top,
+                right,
+                bottom
+            )
+        }
+        return Rect.Zero
+    }
+
+    // This is for PressGestureDetector to cancel the selection.
+    fun onRelease() {
+        selectionRegistrar.subselections = emptyMap()
+        hideSelectionToolbar()
+        if (selection != null) {
+            onSelectionChange(null)
+            hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
+        }
+    }
+
+    fun handleDragObserver(isStartHandle: Boolean): TextDragObserver = object : TextDragObserver {
+        override fun onDown(point: Offset) {
+            val selection = selection ?: return
+            val anchor = if (isStartHandle) selection.start else selection.end
+            val selectable = getAnchorSelectable(anchor) ?: return
+            // The LayoutCoordinates of the composable where the drag gesture should begin. This
+            // is used to convert the position of the beginning of the drag gesture from the
+            // composable coordinates to selection container coordinates.
+            val beginLayoutCoordinates = selectable.getLayoutCoordinates() ?: return
+
+            // The position of the character where the drag gesture should begin. This is in
+            // the composable coordinates.
+            val beginCoordinates = getAdjustedCoordinates(
+                selectable.getHandlePosition(
+                    selection = selection, isStartHandle = isStartHandle
+                )
+            )
+
+            // Convert the position where drag gesture begins from composable coordinates to
+            // selection container coordinates.
+            currentDragPosition = requireContainerCoordinates().localPositionOf(
+                beginLayoutCoordinates,
+                beginCoordinates
+            )
+            draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
+        }
+
+        override fun onUp() {
+            draggingHandle = null
+            currentDragPosition = null
+        }
+
+        override fun onStart(startPoint: Offset) {
+            hideSelectionToolbar()
+            val selection = selection!!
+            val startSelectable =
+                selectionRegistrar.selectableMap[selection.start.selectableId]
+            val endSelectable =
+                selectionRegistrar.selectableMap[selection.end.selectableId]
+            // The LayoutCoordinates of the composable where the drag gesture should begin. This
+            // is used to convert the position of the beginning of the drag gesture from the
+            // composable coordinates to selection container coordinates.
+            val beginLayoutCoordinates = if (isStartHandle) {
+                startSelectable?.getLayoutCoordinates()!!
+            } else {
+                endSelectable?.getLayoutCoordinates()!!
+            }
+
+            // The position of the character where the drag gesture should begin. This is in
+            // the composable coordinates.
+            val beginCoordinates = getAdjustedCoordinates(
+                if (isStartHandle) {
+                    startSelectable!!.getHandlePosition(
+                        selection = selection, isStartHandle = true
+                    )
+                } else {
+                    endSelectable!!.getHandlePosition(
+                        selection = selection, isStartHandle = false
+                    )
+                }
+            )
+
+            // Convert the position where drag gesture begins from composable coordinates to
+            // selection container coordinates.
+            dragBeginPosition = requireContainerCoordinates().localPositionOf(
+                beginLayoutCoordinates,
+                beginCoordinates
+            )
+
+            // Zero out the total distance that being dragged.
+            dragTotalDistance = Offset.Zero
+        }
+
+        override fun onDrag(delta: Offset) {
+            dragTotalDistance += delta
+            val endPosition = dragBeginPosition + dragTotalDistance
+            val consumed = updateSelection(
+                newPosition = endPosition,
+                previousPosition = dragBeginPosition,
+                isStartHandle = isStartHandle,
+                adjustment = SelectionAdjustment.CharacterWithWordAccelerate
+            )
+            if (consumed) {
+                dragBeginPosition = endPosition
+                dragTotalDistance = Offset.Zero
+            }
+        }
+
+        override fun onStop() {
+            showSelectionToolbar()
+            draggingHandle = null
+            currentDragPosition = null
+        }
+
+        override fun onCancel() {
+            showSelectionToolbar()
+            draggingHandle = null
+            currentDragPosition = null
+        }
+    }
+
+    /**
+     * Detect tap without consuming the up event.
+     */
+    private suspend fun PointerInputScope.detectNonConsumingTap(onTap: (Offset) -> Unit) {
+        awaitEachGesture {
+            waitForUpOrCancellation()?.let {
+                onTap(it.position)
+            }
+        }
+    }
+
+    private fun Modifier.onClearSelectionRequested(block: () -> Unit): Modifier {
+        return if (hasFocus) pointerInput(Unit) { detectNonConsumingTap { block() } } else this
+    }
+
+    private fun convertToContainerCoordinates(
+        layoutCoordinates: LayoutCoordinates,
+        offset: Offset
+    ): Offset? {
+        val coordinates = containerLayoutCoordinates
+        if (coordinates == null || !coordinates.isAttached) return null
+        return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
+    }
+
+    /**
+     * Cancel the previous selection and start a new selection at the given [position].
+     * It's used for long-press, double-click, triple-click and so on to start selection.
+     *
+     * @param position initial position of the selection. Both start and end handle is considered
+     * at this position.
+     * @param isStartHandle whether it's considered as the start handle moving. This parameter
+     * will influence the [SelectionAdjustment]'s behavior. For example,
+     * [SelectionAdjustment.Character] only adjust the moving handle.
+     * @param adjustment the selection adjustment.
+     */
+    private fun startSelection(
+        position: Offset,
+        isStartHandle: Boolean,
+        adjustment: SelectionAdjustment
+    ) {
+        updateSelection(
+            startHandlePosition = position,
+            endHandlePosition = position,
+            previousHandlePosition = null,
+            isStartHandle = isStartHandle,
+            adjustment = adjustment
+        )
+    }
+
+    /**
+     * Updates the selection after one of the selection handle moved.
+     *
+     * @param newPosition the new position of the moving selection handle.
+     * @param previousPosition the previous position of the moving selection handle.
+     * @param isStartHandle whether the moving selection handle is the start handle.
+     * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
+     * produce the final selection range.
+     *
+     * @return a boolean representing whether the movement is consumed.
+     *
+     * @see SelectionAdjustment
+     */
+    internal fun updateSelection(
+        newPosition: Offset?,
+        previousPosition: Offset?,
+        isStartHandle: Boolean,
+        adjustment: SelectionAdjustment,
+    ): Boolean {
+        if (newPosition == null) return false
+        val otherHandlePosition = selection?.let { selection ->
+            val otherSelectableId = if (isStartHandle) {
+                selection.end.selectableId
+            } else {
+                selection.start.selectableId
+            }
+            val otherSelectable =
+                selectionRegistrar.selectableMap[otherSelectableId] ?: return@let null
+            convertToContainerCoordinates(
+                otherSelectable.getLayoutCoordinates()!!,
+                getAdjustedCoordinates(
+                    otherSelectable.getHandlePosition(selection, !isStartHandle)
+                )
+            )
+        } ?: return false
+
+        val startHandlePosition = if (isStartHandle) newPosition else otherHandlePosition
+        val endHandlePosition = if (isStartHandle) otherHandlePosition else newPosition
+
+        return updateSelection(
+            startHandlePosition = startHandlePosition,
+            endHandlePosition = endHandlePosition,
+            previousHandlePosition = previousPosition,
+            isStartHandle = isStartHandle,
+            adjustment = adjustment
+        )
+    }
+
+    /**
+     * Updates the selection after one of the selection handle moved.
+     *
+     * To make sure that [SelectionAdjustment] works correctly, it's expected that only one
+     * selection handle is updated each time. The only exception is that when a new selection is
+     * started. In this case, [previousHandlePosition] is always null.
+     *
+     * @param startHandlePosition the position of the start selection handle.
+     * @param endHandlePosition the position of the end selection handle.
+     * @param previousHandlePosition the position of the moving handle before the update.
+     * @param isStartHandle whether the moving selection handle is the start handle.
+     * @param adjustment the [SelectionAdjustment] used to adjust the raw selection range and
+     * produce the final selection range.
+     *
+     * @return a boolean representing whether the movement is consumed. It's useful for the case
+     * where a selection handle is updating consecutively. When the return value is true, it's
+     * expected that the caller will update the [startHandlePosition] to be the given
+     * [endHandlePosition] in following calls.
+     *
+     * @see SelectionAdjustment
+     */
+    internal fun updateSelection(
+        startHandlePosition: Offset,
+        endHandlePosition: Offset,
+        previousHandlePosition: Offset?,
+        isStartHandle: Boolean,
+        adjustment: SelectionAdjustment,
+    ): Boolean {
+        draggingHandle = if (isStartHandle) Handle.SelectionStart else Handle.SelectionEnd
+        currentDragPosition = if (isStartHandle) startHandlePosition else endHandlePosition
+        val newSubselections = mutableMapOf<Long, Selection>()
+        var moveConsumed = false
+        val newSelection = selectionRegistrar.sort(requireContainerCoordinates())
+            .fastFold(null) { mergedSelection: Selection?, selectable: Selectable ->
+                val previousSubselection =
+                    selectionRegistrar.subselections[selectable.selectableId]
+                val (selection, consumed) = selectable.updateSelection(
+                    startHandlePosition = startHandlePosition,
+                    endHandlePosition = endHandlePosition,
+                    previousHandlePosition = previousHandlePosition,
+                    isStartHandle = isStartHandle,
+                    containerLayoutCoordinates = requireContainerCoordinates(),
+                    adjustment = adjustment,
+                    previousSelection = previousSubselection,
+                )
+
+                moveConsumed = moveConsumed || consumed
+                selection?.let { newSubselections[selectable.selectableId] = it }
+                merge(mergedSelection, selection)
+            }
+        if (newSelection != selection) {
+            hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
+            selectionRegistrar.subselections = newSubselections
+            onSelectionChange(newSelection)
+        }
+        return moveConsumed
+    }
+
+    fun contextMenuOpenAdjustment(position: Offset) {
+        val isEmptySelection = selection?.toTextRange()?.collapsed ?: true
+        // TODO(b/209483184) the logic should be more complex here, it should check that current
+        // selection doesn't include click position
+        if (isEmptySelection) {
+            startSelection(
+                position = position,
+                isStartHandle = true,
+                adjustment = SelectionAdjustment.Word
+            )
+        }
+    }
+}
+
+internal fun merge(lhs: Selection?, rhs: Selection?): Selection? {
+    return lhs?.merge(rhs) ?: rhs
+}
+
+internal expect fun isCopyKeyEvent(keyEvent: KeyEvent): Boolean
+
+internal expect fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier
+
+internal fun calculateSelectionMagnifierCenterAndroid(
+    manager: SelectionManager,
+    magnifierSize: IntSize
+): Offset {
+    fun getMagnifierCenter(anchor: Selection.AnchorInfo, isStartHandle: Boolean): Offset {
+        val selectable = manager.getAnchorSelectable(anchor) ?: return Offset.Unspecified
+        val containerCoordinates = manager.containerLayoutCoordinates ?: return Offset.Unspecified
+        val selectableCoordinates = selectable.getLayoutCoordinates() ?: return Offset.Unspecified
+        // The end offset is exclusive.
+        val offset = if (isStartHandle) anchor.offset else anchor.offset - 1
+
+        // The horizontal position doesn't snap to cursor positions but should directly track the
+        // actual drag.
+        val localDragPosition = selectableCoordinates.localPositionOf(
+            containerCoordinates,
+            manager.currentDragPosition!!
+        )
+        val dragX = localDragPosition.x
+        // But it is constrained by the horizontal bounds of the current line.
+        val centerX = selectable.getRangeOfLineContaining(offset).let { line ->
+            val lineMin = selectable.getBoundingBox(line.min)
+            // line.end is exclusive, but we want the bounding box of the actual last character in
+            // the line.
+            val lineMax = selectable.getBoundingBox((line.max - 1).coerceAtLeast(line.min))
+            val minX = minOf(lineMin.left, lineMax.left)
+            val maxX = maxOf(lineMin.right, lineMax.right)
+            dragX.coerceIn(minX, maxX)
+        }
+
+        // Hide the magnifier when dragged too far (outside the horizontal bounds of how big the
+        // magnifier actually is). See
+        // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/widget/Editor.java;l=5228-5231;drc=2fdb6bd709be078b72f011334362456bb758922c
+        if ((dragX - centerX).absoluteValue > magnifierSize.width / 2) {
+            return Offset.Unspecified
+        }
+
+        // Let the selectable determine the vertical position of the magnifier, since it should be
+        // clamped to the center of text lines.
+        val anchorBounds = selectable.getBoundingBox(offset)
+        val centerY = anchorBounds.center.y
+
+        return containerCoordinates.localPositionOf(
+            sourceCoordinates = selectableCoordinates,
+            relativeToSource = Offset(centerX, centerY)
+        )
+    }
+
+    val selection = manager.selection ?: return Offset.Unspecified
+    return when (manager.draggingHandle) {
+        null -> return Offset.Unspecified
+        Handle.SelectionStart -> getMagnifierCenter(selection.start, isStartHandle = true)
+        Handle.SelectionEnd -> getMagnifierCenter(selection.end, isStartHandle = false)
+        Handle.Cursor -> error("SelectionContainer does not support cursor")
+    }
+}
+
+internal fun getCurrentSelectedText(
+    selectable: Selectable,
+    selection: Selection
+): AnnotatedString {
+    val currentText = selectable.getText()
+
+    return if (
+        selectable.selectableId != selection.start.selectableId &&
+        selectable.selectableId != selection.end.selectableId
+    ) {
+        // Select the full text content if the current selectable is between the
+        // start and the end selectables.
+        currentText
+    } else if (
+        selectable.selectableId == selection.start.selectableId &&
+        selectable.selectableId == selection.end.selectableId
+    ) {
+        // Select partial text content if the current selectable is the start and
+        // the end selectable.
+        if (selection.handlesCrossed) {
+            currentText.subSequence(selection.end.offset, selection.start.offset)
+        } else {
+            currentText.subSequence(selection.start.offset, selection.end.offset)
+        }
+    } else if (selectable.selectableId == selection.start.selectableId) {
+        // Select partial text content if the current selectable is the start
+        // selectable.
+        if (selection.handlesCrossed) {
+            currentText.subSequence(0, selection.start.offset)
+        } else {
+            currentText.subSequence(selection.start.offset, currentText.length)
+        }
+    } else {
+        // Selectable partial text content if the current selectable is the end
+        // selectable.
+        if (selection.handlesCrossed) {
+            currentText.subSequence(selection.end.offset, currentText.length)
+        } else {
+            currentText.subSequence(0, selection.end.offset)
+        }
+    }
+}
+
+/** Returns the boundary of the visible area in this [LayoutCoordinates]. */
+internal fun LayoutCoordinates.visibleBounds(): Rect {
+    // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
+    // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
+    // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
+    val boundsInWindow = boundsInWindow()
+    return Rect(
+        windowToLocal(boundsInWindow.topLeft),
+        windowToLocal(boundsInWindow.bottomRight)
+    )
+}
+
+internal fun Rect.containsInclusive(offset: Offset): Boolean =
+    offset.x in left..right && offset.y in top..bottom
+
+internal enum class Handle {
+    Cursor,
+    SelectionStart,
+    SelectionEnd
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionMode.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionMode.kt
new file mode 100644
index 0000000..da7e8e0
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionMode.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.newtext.text.copypasta.selection
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+
+/**
+ * The enum class allows user to decide the selection mode.
+ */
+internal enum class SelectionMode {
+    /**
+     * When selection handles are dragged across composables, selection extends by row, for example,
+     * when the end selection handle is dragged down, upper rows will be selected first, and the
+     * lower rows.
+     */
+    Vertical {
+        override fun compare(position: Offset, bounds: Rect): Int {
+            if (bounds.contains(position)) return 0
+
+            // When the position of the selection handle is on the top of the composable, and the
+            // not on the right of the composable, it's considered as start.
+            if (position.y < bounds.top) return -1
+
+            // When the position of the selection handle is on the left of the composable, and not
+            // below the bottom of composable, it's considered as start.
+            if (position.x < bounds.left && position.y < bounds.bottom) return -1
+
+            // In all other cases, the selection handle is considered as the end.
+            return 1
+        }
+    },
+
+    /**
+     * When selection handles are dragged across composables, selection extends by column, for example,
+     * when the end selection handle is dragged to the right, left columns will be selected first,
+     * and the right rows.
+     */
+    Horizontal {
+        override fun compare(position: Offset, bounds: Rect): Int {
+            if (bounds.contains(position)) return 0
+
+            // When the end of the selection is on the left of the composable, the composable is
+            // outside of the selection range.
+            if (position.x < bounds.left) return -1
+
+            // When the end of the selection is on the top of the composable, and the not on the
+            // right of the composable, the composable is outside of the selection range.
+            if (position.y < bounds.top && position.x < bounds.right) return -1
+
+            // In all other cases, the selection handle is considered as the end.
+            return 1
+        }
+    };
+
+    /**
+     * A compare a selection handle with a  [Selectable] boundary. This defines whether an out of
+     * boundary selection handle is treated as the start or the end of the Selectable. If the
+     * [Selectable] is a text selectable, then the start is the index 0, and end corresponds to
+     * the text length.
+     *
+     * @param position the position of the selection handle.
+     * @param bounds the boundary of the [Selectable].
+     * @return 0 if the selection handle [position] is within the [bounds]; a negative value if
+     * the selection handle is considered as "start" of the [Selectable]; a positive value if the
+     * selection handle is considered as the "end" of the [Selectable].
+     */
+    internal abstract fun compare(position: Offset, bounds: Rect): Int
+
+    /**
+     * Decides if Composable which has [bounds], should be accepted by the selection and
+     * change its selected state for a selection that starts at [start] and ends at [end].
+     *
+     * @param bounds Composable bounds of the widget to be checked.
+     * @param start The start coordinates of the selection, in SelectionContainer range.
+     * @param end The end coordinates of the selection, in SelectionContainer range.
+     */
+    internal fun isSelected(
+        bounds: Rect,
+        start: Offset,
+        end: Offset
+    ): Boolean {
+        // If either of the start or end is contained by bounds, the composable is selected.
+        if (bounds.contains(start) || bounds.contains(end)) {
+            return true
+        }
+        // Compare the location of start and end to the bound. If both are on the same side, return
+        // false, otherwise return true.
+        val compareStart = compare(start, bounds)
+        val compareEnd = compare(end, bounds)
+        return (compareStart > 0) xor (compareEnd > 0)
+    }
+}
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionRegistrar.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionRegistrar.kt
new file mode 100644
index 0000000..dd1f86c
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionRegistrar.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.copypasta.selection
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.LayoutCoordinates
+
+/**
+ *  An interface allowing a composable to subscribe and unsubscribe to selection changes.
+ */
+internal interface SelectionRegistrar {
+    /**
+     * The map stored current selection information on each [Selectable]. A selectable can query
+     * its selected range using its [Selectable.selectableId]. This field is backed by a
+     * [MutableState]. And any composable reading this field will be recomposed once its value
+     * changed.
+     */
+    val subselections: Map<Long, Selection>
+
+    /**
+     * Subscribe to SelectionContainer selection changes.
+     * @param selectable the [Selectable] that is subscribing to this [SelectionRegistrar].
+     */
+    fun subscribe(selectable: Selectable): Selectable
+
+    /**
+     * Unsubscribe from SelectionContainer selection changes.
+     * @param selectable the [Selectable] that is unsubscribing to this [SelectionRegistrar].
+     */
+    fun unsubscribe(selectable: Selectable)
+
+    /**
+     * Return a unique ID for a [Selectable].
+     * @see [Selectable.selectableId]
+     */
+    fun nextSelectableId(): Long
+
+    /**
+     * When the Global Position of a subscribed [Selectable] changes, this method
+     * is called.
+     */
+    fun notifyPositionChange(selectableId: Long)
+
+    /**
+     * Call this method to notify the [SelectionContainer] that the selection has been initiated.
+     * Depends on the input, [notifySelectionUpdate] may be called repeatedly after
+     * [notifySelectionUpdateStart] is called. And [notifySelectionUpdateEnd] should always be
+     * called after selection finished.
+     * For example:
+     *  1. User long pressed the text and then release. [notifySelectionUpdateStart] should be
+     *  called followed by [notifySelectionUpdateEnd] being called once.
+     *  2. User long pressed the text and then drag a distance and then release.
+     *  [notifySelectionUpdateStart] should be called first after the user long press, and then
+     *  [notifySelectionUpdate] is called several times reporting the updates, in the end
+     *  [notifySelectionUpdateEnd] is called to finish the selection.
+     *
+     * @param layoutCoordinates [LayoutCoordinates] of the [Selectable].
+     * @param startPosition coordinates of where the selection is initiated.
+     * @param adjustment selection should be adjusted according to this param
+     *
+     * @see notifySelectionUpdate
+     * @see notifySelectionUpdateEnd
+     */
+    fun notifySelectionUpdateStart(
+        layoutCoordinates: LayoutCoordinates,
+        startPosition: Offset,
+        adjustment: SelectionAdjustment
+    )
+
+    /**
+     * Call this method to notify the [SelectionContainer] that the selection has been initiated
+     * with selectAll [Selection].
+     *
+     * @param selectableId [selectableId] of the [Selectable]
+     */
+    fun notifySelectionUpdateSelectAll(selectableId: Long)
+
+    /**
+     * Call this method to notify the [SelectionContainer] that one of the selection handle has
+     * moved and selection should be updated.
+     * The caller of this method should make sure that [notifySelectionUpdateStart] is always
+     * called once before calling this function. And [notifySelectionUpdateEnd] is always called
+     * once after the all updates finished.
+     *
+     * @param layoutCoordinates [LayoutCoordinates] of the [Selectable].
+     * @param previousPosition coordinates of where the selection starts.
+     * @param newPosition coordinates of where the selection ends.
+     * @param isStartHandle whether the moving selection handle the start handle.
+     * @param adjustment selection should be adjusted according to this parameter
+     *
+     * @return true if the selection handle movement is consumed. This function acts like a
+     * pointer input consumer when a selection handle is dragged. It expects the caller to
+     * accumulate the unconsumed pointer movement:
+     * 1. if it returns true, the caller will zero out the previous movement.
+     * 2. if it returns false, the caller will continue accumulate pointer movement.
+     * @see notifySelectionUpdateStart
+     * @see notifySelectionUpdateEnd
+     */
+    fun notifySelectionUpdate(
+        layoutCoordinates: LayoutCoordinates,
+        newPosition: Offset,
+        previousPosition: Offset,
+        isStartHandle: Boolean,
+        adjustment: SelectionAdjustment
+    ): Boolean
+
+    /**
+     * Call this method to notify the [SelectionContainer] that the selection update has stopped.
+     *
+     * @see notifySelectionUpdateStart
+     * @see notifySelectionUpdate
+     */
+    fun notifySelectionUpdateEnd()
+
+    /**
+     * Call this method to notify the [SelectionContainer] that the content of the passed
+     * selectable has been changed.
+     *
+     * @param selectableId the ID of the selectable whose the content has been updated.
+     */
+    fun notifySelectableChange(selectableId: Long)
+
+    companion object {
+        /**
+         * Representing an invalid ID for [Selectable].
+         */
+        const val InvalidSelectableId = 0L
+    }
+}
+
+/**
+ * Helper function that checks if there is a selection on this CoreText.
+ */
+internal fun SelectionRegistrar?.hasSelection(selectableId: Long): Boolean {
+    return this?.subselections?.containsKey(selectableId) ?: false
+}
+
+/**
+ * SelectionRegistrar CompositionLocal. Composables that implement selection logic can use this
+ * CompositionLocal to get a [SelectionRegistrar] in order to subscribe and unsubscribe to
+ * [SelectionRegistrar].
+ */
+internal val LocalSelectionRegistrar = compositionLocalOf<SelectionRegistrar?> { null }
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionRegistrarImpl.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionRegistrarImpl.kt
new file mode 100644
index 0000000..f08c238
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionRegistrarImpl.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.copypasta.selection
+
+import androidx.compose.foundation.newtext.text.copypasta.AtomicLong
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.LayoutCoordinates
+
+internal class SelectionRegistrarImpl : SelectionRegistrar {
+    /**
+     * A flag to check if the [Selectable]s have already been sorted.
+     */
+    internal var sorted: Boolean = false
+
+    /**
+     * This is essentially the list of registered components that want
+     * to handle text selection that are below the SelectionContainer.
+     */
+    private val _selectables = mutableListOf<Selectable>()
+
+    /**
+     * Getter for handlers that returns a List.
+     */
+    internal val selectables: List<Selectable>
+        get() = _selectables
+
+    private val _selectableMap = mutableMapOf<Long, Selectable>()
+
+    /**
+     * A map from selectable keys to subscribed selectables.
+     */
+    internal val selectableMap: Map<Long, Selectable>
+        get() = _selectableMap
+
+    /**
+     * The incremental id to be assigned to each selectable. It starts from 1 and 0 is used to
+     * denote an invalid id.
+     * @see SelectionRegistrar.InvalidSelectableId
+     */
+    private var incrementId = AtomicLong(1)
+
+    /**
+     * The callback to be invoked when the position change was triggered.
+     */
+    internal var onPositionChangeCallback: ((Long) -> Unit)? = null
+
+    /**
+     * The callback to be invoked when the selection is initiated.
+     */
+    internal var onSelectionUpdateStartCallback:
+        ((LayoutCoordinates, Offset, SelectionAdjustment) -> Unit)? = null
+
+    /**
+     * The callback to be invoked when the selection is initiated with selectAll [Selection].
+     */
+    internal var onSelectionUpdateSelectAll: (
+        (Long) -> Unit
+    )? = null
+
+    /**
+     * The callback to be invoked when the selection is updated.
+     * If the first offset is null it means that the start of selection is unknown for the caller.
+     */
+    internal var onSelectionUpdateCallback:
+        ((LayoutCoordinates, Offset, Offset, Boolean, SelectionAdjustment) -> Boolean)? = null
+
+    /**
+     * The callback to be invoked when selection update finished.
+     */
+    internal var onSelectionUpdateEndCallback: (() -> Unit)? = null
+
+    /**
+     * The callback to be invoked when one of the selectable has changed.
+     */
+    internal var onSelectableChangeCallback: ((Long) -> Unit)? = null
+
+    /**
+     * The callback to be invoked after a selectable is unsubscribed from this [SelectionRegistrar].
+     */
+    internal var afterSelectableUnsubscribe: ((Long) -> Unit)? = null
+
+    override var subselections: Map<Long, Selection> by mutableStateOf(emptyMap())
+
+    override fun subscribe(selectable: Selectable): Selectable {
+        require(selectable.selectableId != SelectionRegistrar.InvalidSelectableId) {
+            "The selectable contains an invalid id: ${selectable.selectableId}"
+        }
+        require(!_selectableMap.containsKey(selectable.selectableId)) {
+            "Another selectable with the id: $selectable.selectableId has already subscribed."
+        }
+        _selectableMap[selectable.selectableId] = selectable
+        _selectables.add(selectable)
+        sorted = false
+        return selectable
+    }
+
+    override fun unsubscribe(selectable: Selectable) {
+        if (!_selectableMap.containsKey(selectable.selectableId)) return
+        _selectables.remove(selectable)
+        _selectableMap.remove(selectable.selectableId)
+        afterSelectableUnsubscribe?.invoke(selectable.selectableId)
+    }
+
+    override fun nextSelectableId(): Long {
+        var id = incrementId.getAndIncrement()
+        while (id == SelectionRegistrar.InvalidSelectableId) {
+            id = incrementId.getAndIncrement()
+        }
+        return id
+    }
+
+    /**
+     * Sort the list of registered [Selectable]s in [SelectionRegistrar]. Currently the order of
+     * selectables is geometric-based.
+     */
+    fun sort(containerLayoutCoordinates: LayoutCoordinates): List<Selectable> {
+        if (!sorted) {
+            // Sort selectables by y-coordinate first, and then x-coordinate, to match English
+            // hand-writing habit.
+            _selectables.sortWith { a: Selectable, b: Selectable ->
+                val layoutCoordinatesA = a.getLayoutCoordinates()
+                val layoutCoordinatesB = b.getLayoutCoordinates()
+
+                val positionA = if (layoutCoordinatesA != null) {
+                    containerLayoutCoordinates.localPositionOf(layoutCoordinatesA, Offset.Zero)
+                } else {
+                    Offset.Zero
+                }
+                val positionB = if (layoutCoordinatesB != null) {
+                    containerLayoutCoordinates.localPositionOf(layoutCoordinatesB, Offset.Zero)
+                } else {
+                    Offset.Zero
+                }
+
+                if (positionA.y == positionB.y) {
+                    compareValues(positionA.x, positionB.x)
+                } else {
+                    compareValues(positionA.y, positionB.y)
+                }
+            }
+            sorted = true
+        }
+        return selectables
+    }
+
+    override fun notifyPositionChange(selectableId: Long) {
+        // Set the variable sorted to be false, when the global position of a registered
+        // selectable changes.
+        sorted = false
+        onPositionChangeCallback?.invoke(selectableId)
+    }
+
+    override fun notifySelectionUpdateStart(
+        layoutCoordinates: LayoutCoordinates,
+        startPosition: Offset,
+        adjustment: SelectionAdjustment
+    ) {
+        onSelectionUpdateStartCallback?.invoke(layoutCoordinates, startPosition, adjustment)
+    }
+
+    override fun notifySelectionUpdateSelectAll(selectableId: Long) {
+        onSelectionUpdateSelectAll?.invoke(selectableId)
+    }
+
+    override fun notifySelectionUpdate(
+        layoutCoordinates: LayoutCoordinates,
+        newPosition: Offset,
+        previousPosition: Offset,
+        isStartHandle: Boolean,
+        adjustment: SelectionAdjustment
+    ): Boolean {
+        return onSelectionUpdateCallback?.invoke(
+            layoutCoordinates,
+            newPosition,
+            previousPosition,
+            isStartHandle,
+            adjustment
+        ) ?: true
+    }
+
+    override fun notifySelectionUpdateEnd() {
+        onSelectionUpdateEndCallback?.invoke()
+    }
+
+    override fun notifySelectableChange(selectableId: Long) {
+        onSelectableChangeCallback?.invoke(selectableId)
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SimpleLayout.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SimpleLayout.kt
new file mode 100644
index 0000000..9929a2e
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SimpleLayout.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.newtext.text.copypasta.selection
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastMap
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+import kotlin.math.max
+
+/**
+ * Selection is transparent in terms of measurement and layout and passes the same constraints to
+ * the children.
+ */
+@Composable
+internal fun SimpleLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+    Layout(modifier = modifier, content = content) { measurables, constraints ->
+        val placeables = measurables.fastMap { measurable ->
+            measurable.measure(constraints)
+        }
+
+        val width = placeables.fastFold(0) { maxWidth, placeable ->
+            max(maxWidth, (placeable.width))
+        }
+
+        val height = placeables.fastFold(0) { minWidth, placeable ->
+            max(minWidth, (placeable.height))
+        }
+
+        layout(width, height) {
+            placeables.fastForEach { placeable ->
+                placeable.place(0, 0)
+            }
+        }
+    }
+}
+
+// copypasta from foundation to compile this copypasta
+@Suppress("BanInlineOptIn") // Treat Kotlin Contracts as non-experimental.
+@OptIn(ExperimentalContracts::class)
+internal inline fun <T, R> List<T>.fastFold(initial: R, operation: (acc: R, T) -> R): R {
+    contract { callsInPlace(operation) }
+    var accumulator = initial
+    fastForEach { e ->
+        accumulator = operation(accumulator, e)
+    }
+    return accumulator
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/TextPreparedSelection.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/TextPreparedSelection.kt
new file mode 100644
index 0000000..a3e5817
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/TextPreparedSelection.kt
@@ -0,0 +1,432 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.copypasta.selection
+
+import androidx.compose.foundation.newtext.text.copypasta.TextLayoutResultProxy
+import androidx.compose.foundation.newtext.text.copypasta.findFollowingBreak
+import androidx.compose.foundation.newtext.text.copypasta.findParagraphEnd
+import androidx.compose.foundation.newtext.text.copypasta.findParagraphStart
+import androidx.compose.foundation.newtext.text.copypasta.findPrecedingBreak
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.CommitTextCommand
+import androidx.compose.ui.text.input.EditCommand
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.SetSelectionCommand
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.ResolvedTextDirection
+
+internal class TextPreparedSelectionState {
+    // it's set at the start of vertical navigation and used as the preferred value to set a new
+    // cursor position.
+    var cachedX: Float? = null
+
+    fun resetCachedX() {
+        cachedX = null
+    }
+}
+
+/**
+ * This utility class implements many selection-related operations on text (including basic
+ * cursor movements and deletions) and combines them, taking into account how the text was
+ * rendered. So, for example, [moveCursorToLineEnd] moves it to the visual line end.
+ *
+ * For many of these operations, it's particularly important to keep the difference between
+ * selection start and selection end. In some systems, they are called "anchor" and "caret"
+ * respectively. For example, for selection from scratch, after [moveCursorLeftByWord]
+ * [moveCursorRight] will move the left side of the selection, but after [moveCursorRightByWord]
+ * the right one.
+ *
+ * To use it in scope of text fields see [TextFieldPreparedSelection]
+ */
+internal abstract class BaseTextPreparedSelection<T : BaseTextPreparedSelection<T>>(
+    val originalText: AnnotatedString,
+    val originalSelection: TextRange,
+    val layoutResult: TextLayoutResult?,
+    val offsetMapping: OffsetMapping,
+    val state: TextPreparedSelectionState
+) {
+    var selection = originalSelection
+
+    var annotatedString = originalText
+    internal val text
+        get() = annotatedString.text
+
+    @Suppress("UNCHECKED_CAST")
+    protected inline fun <U> U.apply(resetCachedX: Boolean = true, block: U.() -> Unit): T {
+        if (resetCachedX) {
+            state.resetCachedX()
+        }
+        if (text.isNotEmpty()) {
+            block()
+        }
+        return this as T
+    }
+
+    protected fun setCursor(offset: Int) {
+        setSelection(offset, offset)
+    }
+
+    protected fun setSelection(start: Int, end: Int) {
+        selection = TextRange(start, end)
+    }
+
+    fun selectAll() = apply {
+        setSelection(0, text.length)
+    }
+
+    fun deselect() = apply {
+        setCursor(selection.end)
+    }
+
+    fun moveCursorLeft() = apply {
+        if (isLtr()) {
+            moveCursorPrev()
+        } else {
+            moveCursorNext()
+        }
+    }
+
+    fun moveCursorRight() = apply {
+        if (isLtr()) {
+            moveCursorNext()
+        } else {
+            moveCursorPrev()
+        }
+    }
+
+    /**
+     * If there is already a selection, collapse it to the left side. Otherwise, execute [or]
+     */
+    fun collapseLeftOr(or: T.() -> Unit) = apply {
+        if (selection.collapsed) {
+            @Suppress("UNCHECKED_CAST")
+            or(this as T)
+        } else {
+            if (isLtr()) {
+                setCursor(selection.min)
+            } else {
+                setCursor(selection.max)
+            }
+        }
+    }
+
+    /**
+     * If there is already a selection, collapse it to the right side. Otherwise, execute [or]
+     */
+    fun collapseRightOr(or: T.() -> Unit) = apply {
+        if (selection.collapsed) {
+            @Suppress("UNCHECKED_CAST")
+            or(this as T)
+        } else {
+            if (isLtr()) {
+                setCursor(selection.max)
+            } else {
+                setCursor(selection.min)
+            }
+        }
+    }
+
+    /**
+     * Returns the index of the character break preceding the end of [selection].
+     */
+    fun getPrecedingCharacterIndex() = annotatedString.text.findPrecedingBreak(selection.end)
+
+    /**
+     * Returns the index of the character break following the end of [selection]. Returns
+     * [NoCharacterFound] if there are no more breaks before the end of the string.
+     */
+    fun getNextCharacterIndex() = annotatedString.text.findFollowingBreak(selection.end)
+
+    private fun moveCursorPrev() = apply {
+        val prev = getPrecedingCharacterIndex()
+        if (prev != -1) setCursor(prev)
+    }
+
+    private fun moveCursorNext() = apply {
+        val next = getNextCharacterIndex()
+        if (next != -1) setCursor(next)
+    }
+
+    fun moveCursorToHome() = apply {
+        setCursor(0)
+    }
+
+    fun moveCursorToEnd() = apply {
+        setCursor(text.length)
+    }
+
+    fun moveCursorLeftByWord() = apply {
+        if (isLtr()) {
+            moveCursorPrevByWord()
+        } else {
+            moveCursorNextByWord()
+        }
+    }
+
+    fun moveCursorRightByWord() = apply {
+        if (isLtr()) {
+            moveCursorNextByWord()
+        } else {
+            moveCursorPrevByWord()
+        }
+    }
+
+    fun getNextWordOffset(): Int? = layoutResult?.getNextWordOffsetForLayout()
+
+    private fun moveCursorNextByWord() = apply {
+        getNextWordOffset()?.let { setCursor(it) }
+    }
+
+    fun getPreviousWordOffset(): Int? = layoutResult?.getPrevWordOffset()
+
+    private fun moveCursorPrevByWord() = apply {
+        getPreviousWordOffset()?.let { setCursor(it) }
+    }
+
+    fun moveCursorPrevByParagraph() = apply {
+        setCursor(getParagraphStart())
+    }
+
+    fun moveCursorNextByParagraph() = apply {
+        setCursor(getParagraphEnd())
+    }
+
+    fun moveCursorUpByLine() = apply(false) {
+        layoutResult?.jumpByLinesOffset(-1)?.let { setCursor(it) }
+    }
+
+    fun moveCursorDownByLine() = apply(false) {
+        layoutResult?.jumpByLinesOffset(1)?.let { setCursor(it) }
+    }
+
+    fun getLineStartByOffset(): Int? = layoutResult?.getLineStartByOffsetForLayout()
+
+    fun moveCursorToLineStart() = apply {
+        getLineStartByOffset()?.let { setCursor(it) }
+    }
+
+    fun getLineEndByOffset(): Int? = layoutResult?.getLineEndByOffsetForLayout()
+
+    fun moveCursorToLineEnd() = apply {
+        getLineEndByOffset()?.let { setCursor(it) }
+    }
+
+    fun moveCursorToLineLeftSide() = apply {
+        if (isLtr()) {
+            moveCursorToLineStart()
+        } else {
+            moveCursorToLineEnd()
+        }
+    }
+
+    fun moveCursorToLineRightSide() = apply {
+        if (isLtr()) {
+            moveCursorToLineEnd()
+        } else {
+            moveCursorToLineStart()
+        }
+    }
+
+    // it selects a text from the original selection start to a current selection end
+    fun selectMovement() = apply(false) {
+        selection = TextRange(originalSelection.start, selection.end)
+    }
+
+    private fun isLtr(): Boolean {
+        val direction = layoutResult?.getParagraphDirection(transformedEndOffset())
+        return direction != ResolvedTextDirection.Rtl
+    }
+
+    private fun TextLayoutResult.getNextWordOffsetForLayout(
+        currentOffset: Int = transformedEndOffset()
+    ): Int {
+        if (currentOffset >= originalText.length) {
+            return originalText.length
+        }
+        val currentWord = getWordBoundary(charOffset(currentOffset))
+        return if (currentWord.end <= currentOffset) {
+            getNextWordOffsetForLayout(currentOffset + 1)
+        } else {
+            offsetMapping.transformedToOriginal(currentWord.end)
+        }
+    }
+
+    private fun TextLayoutResult.getPrevWordOffset(
+        currentOffset: Int = transformedEndOffset()
+    ): Int {
+        if (currentOffset < 0) {
+            return 0
+        }
+        val currentWord = getWordBoundary(charOffset(currentOffset))
+        return if (currentWord.start >= currentOffset) {
+            getPrevWordOffset(currentOffset - 1)
+        } else {
+            offsetMapping.transformedToOriginal(currentWord.start)
+        }
+    }
+
+    private fun TextLayoutResult.getLineStartByOffsetForLayout(
+        currentOffset: Int = transformedMinOffset()
+    ): Int {
+        val currentLine = getLineForOffset(currentOffset)
+        return offsetMapping.transformedToOriginal(getLineStart(currentLine))
+    }
+
+    private fun TextLayoutResult.getLineEndByOffsetForLayout(
+        currentOffset: Int = transformedMaxOffset()
+    ): Int {
+        val currentLine = getLineForOffset(currentOffset)
+        return offsetMapping.transformedToOriginal(getLineEnd(currentLine, true))
+    }
+
+    private fun TextLayoutResult.jumpByLinesOffset(linesAmount: Int): Int {
+        val currentOffset = transformedEndOffset()
+
+        if (state.cachedX == null) {
+            state.cachedX = getCursorRect(currentOffset).left
+        }
+
+        val targetLine = getLineForOffset(currentOffset) + linesAmount
+        when {
+            targetLine < 0 -> {
+                return 0
+            }
+            targetLine >= lineCount -> {
+                return text.length
+            }
+        }
+
+        val y = getLineBottom(targetLine) - 1
+        val x = state.cachedX!!.also {
+            if ((isLtr() && it >= getLineRight(targetLine)) ||
+                (!isLtr() && it <= getLineLeft(targetLine))
+            ) {
+                return getLineEnd(targetLine, true)
+            }
+        }
+
+        val newOffset = getOffsetForPosition(Offset(x, y)).let {
+            offsetMapping.transformedToOriginal(it)
+        }
+
+        return newOffset
+    }
+
+    private fun transformedEndOffset(): Int {
+        return offsetMapping.originalToTransformed(selection.end)
+    }
+
+    private fun transformedMinOffset(): Int {
+        return offsetMapping.originalToTransformed(selection.min)
+    }
+
+    private fun transformedMaxOffset(): Int {
+        return offsetMapping.originalToTransformed(selection.max)
+    }
+
+    private fun charOffset(offset: Int) =
+        offset.coerceAtMost(text.length - 1)
+
+    private fun getParagraphStart() = text.findParagraphStart(selection.min)
+
+    private fun getParagraphEnd() = text.findParagraphEnd(selection.max)
+
+    companion object {
+        /**
+         * Value returned by [getNextCharacterIndex] and [getPrecedingCharacterIndex] when no valid
+         * index could be found, e.g. it would be the end of the string.
+         *
+         * This is equivalent to `BreakIterator.DONE` on JVM/Android.
+         */
+        const val NoCharacterFound = -1
+    }
+}
+
+internal class TextPreparedSelection(
+    originalText: AnnotatedString,
+    originalSelection: TextRange,
+    layoutResult: TextLayoutResult? = null,
+    offsetMapping: OffsetMapping = OffsetMapping.Identity,
+    state: TextPreparedSelectionState = TextPreparedSelectionState()
+) : BaseTextPreparedSelection<TextPreparedSelection>(
+    originalText = originalText,
+    originalSelection = originalSelection,
+    layoutResult = layoutResult,
+    offsetMapping = offsetMapping,
+    state = state
+)
+
+internal class TextFieldPreparedSelection(
+    val currentValue: TextFieldValue,
+    offsetMapping: OffsetMapping = OffsetMapping.Identity,
+    val layoutResultProxy: TextLayoutResultProxy?,
+    state: TextPreparedSelectionState = TextPreparedSelectionState()
+) : BaseTextPreparedSelection<TextFieldPreparedSelection>(
+    originalText = currentValue.annotatedString,
+    originalSelection = currentValue.selection,
+    offsetMapping = offsetMapping,
+    layoutResult = layoutResultProxy?.value,
+    state = state
+) {
+    val value
+        get() = currentValue.copy(
+            annotatedString = annotatedString,
+            selection = selection
+        )
+
+    fun deleteIfSelectedOr(or: TextFieldPreparedSelection.() -> EditCommand?): List<EditCommand>? {
+        return if (selection.collapsed) {
+            or(this)?.let {
+                listOf(it)
+            }
+        } else {
+            listOf(
+                CommitTextCommand("", 0),
+                SetSelectionCommand(selection.min, selection.min)
+            )
+        }
+    }
+
+    fun moveCursorUpByPage() = apply(false) {
+        layoutResultProxy?.jumpByPagesOffset(-1)?.let { setCursor(it) }
+    }
+
+    fun moveCursorDownByPage() = apply(false) {
+        layoutResultProxy?.jumpByPagesOffset(1)?.let { setCursor(it) }
+    }
+
+    /**
+     * Returns a cursor position after jumping back or forth by [pagesAmount] number of pages,
+     * where `page` is the visible amount of space in the text field
+     */
+    private fun TextLayoutResultProxy.jumpByPagesOffset(pagesAmount: Int): Int {
+        val visibleInnerTextFieldRect = innerTextFieldCoordinates?.let { inner ->
+            decorationBoxCoordinates?.localBoundingBoxOf(inner)
+        } ?: Rect.Zero
+        val currentOffset = offsetMapping.originalToTransformed(currentValue.selection.end)
+        val currentPos = value.getCursorRect(currentOffset)
+        val x = currentPos.left
+        val y = currentPos.top + visibleInnerTextFieldRect.size.height * pagesAmount
+        return offsetMapping.transformedToOriginal(
+            value.getOffsetForPosition(Offset(x, y))
+        )
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/TextSelectionColors.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/TextSelectionColors.kt
new file mode 100644
index 0000000..6cc76ee
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/TextSelectionColors.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.compose.foundation.newtext.text.copypasta.selection
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.ui.graphics.Color
+
+/**
+ * Represents the colors used for text selection by text and text field components.
+ *
+ * See [LocalTextSelectionColors] to provide new values for this throughout the hierarchy.
+ *
+ * @property handleColor the color used for the selection handles on either side of the
+ * selection region.
+ * @property backgroundColor the color used to draw the background behind the selected
+ * region. This color should have alpha applied to keep the text legible - this alpha is
+ * typically 0.4f (40%) but this may need to be reduced in order to meet contrast requirements
+ * depending on the color used for text, selection background, and the background behind the
+ * selection background.
+ */
+@Immutable
+class TextSelectionColors(
+    val handleColor: Color,
+    val backgroundColor: Color
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is TextSelectionColors) return false
+
+        if (handleColor != other.handleColor) return false
+        if (backgroundColor != other.backgroundColor) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = handleColor.hashCode()
+        result = 31 * result + backgroundColor.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "SelectionColors(selectionHandleColor=$handleColor, " +
+            "selectionBackgroundColor=$backgroundColor)"
+    }
+}
+
+/**
+ * CompositionLocal used to change the [TextSelectionColors] used by text and text field
+ * components in the hierarchy.
+ */
+val LocalTextSelectionColors = compositionLocalOf { DefaultTextSelectionColors }
+
+/**
+ * Default color used is the blue from the Compose logo, b/172679845 for context
+ */
+private val DefaultSelectionColor = Color(0xFF4286F4)
+
+@Stable
+private val DefaultTextSelectionColors = TextSelectionColors(
+    handleColor = DefaultSelectionColor,
+    backgroundColor = DefaultSelectionColor.copy(alpha = 0.4f)
+)
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/TextSelectionDelegate.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/TextSelectionDelegate.kt
new file mode 100644
index 0000000..5a530ec
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/TextSelectionDelegate.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.newtext.text.copypasta.selection
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.text.TextLayoutResult
+import kotlin.math.max
+
+/**
+ * This method returns the graphical position where the selection handle should be based on the
+ * offset and other information.
+ *
+ * @param textLayoutResult a result of the text layout.
+ * @param offset character offset to be calculated
+ * @param isStart true if called for selection start handle
+ * @param areHandlesCrossed true if the selection handles are crossed
+ *
+ * @return the graphical position where the selection handle should be.
+ */
+internal fun getSelectionHandleCoordinates(
+    textLayoutResult: TextLayoutResult,
+    offset: Int,
+    isStart: Boolean,
+    areHandlesCrossed: Boolean
+): Offset {
+    val line = textLayoutResult.getLineForOffset(offset)
+    val x = textLayoutResult.getHorizontalPosition(offset, isStart, areHandlesCrossed)
+    val y = textLayoutResult.getLineBottom(line)
+
+    return Offset(x, y)
+}
+
+internal fun TextLayoutResult.getHorizontalPosition(
+    offset: Int,
+    isStart: Boolean,
+    areHandlesCrossed: Boolean
+): Float {
+    val offsetToCheck =
+        if (isStart && !areHandlesCrossed || !isStart && areHandlesCrossed) offset
+        else max(offset - 1, 0)
+    val bidiRunDirection = getBidiRunDirection(offsetToCheck)
+    val paragraphDirection = getParagraphDirection(offset)
+
+    return getHorizontalPosition(
+        offset = offset,
+        usePrimaryDirection = bidiRunDirection == paragraphDirection
+    )
+}
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/TextSelectionMouseDetector.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/TextSelectionMouseDetector.kt
new file mode 100644
index 0000000..3931bc4
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/TextSelectionMouseDetector.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.compose.foundation.newtext.text.copypasta.selection
+
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.drag
+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.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.input.pointer.changedToDown
+import androidx.compose.ui.input.pointer.isPrimaryPressed
+import androidx.compose.ui.input.pointer.isShiftPressed
+import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.util.fastAll
+
+// * Without shift it starts the new selection from the scratch.
+// * With shift expand / shrink existed selection.
+// * Click sets start and end of the selection, but shift click only the end of
+// selection.
+// * The specific case of it when selection is collapsed, but the same logic is
+// applied for not collapsed selection too.
+internal interface MouseSelectionObserver {
+    // on start of shift click. if returns true event will be consumed
+    fun onExtend(downPosition: Offset): Boolean
+    // on drag after shift click. if returns true event will be consumed
+    fun onExtendDrag(dragPosition: Offset): Boolean
+
+    // if returns true event will be consumed
+    fun onStart(downPosition: Offset, adjustment: SelectionAdjustment): Boolean
+    fun onDrag(dragPosition: Offset, adjustment: SelectionAdjustment): Boolean
+}
+
+// Distance in pixels between consecutive click positions to be considered them as clicks sequence
+internal const val ClicksSlop = 100.0
+
+private class ClicksCounter(
+    private val viewConfiguration: ViewConfiguration
+) {
+    var clicks = 0
+    var prevClick: PointerInputChange? = null
+    fun update(event: PointerEvent) {
+        val currentPrevClick = prevClick
+        val newClick = event.changes[0]
+        if (currentPrevClick != null &&
+            timeIsTolerable(currentPrevClick, newClick) &&
+            positionIsTolerable(currentPrevClick, newClick)
+        ) {
+            clicks += 1
+        } else {
+            clicks = 1
+        }
+        prevClick = newClick
+    }
+
+    fun timeIsTolerable(prevClick: PointerInputChange, newClick: PointerInputChange): Boolean {
+        val diff = newClick.uptimeMillis - prevClick.uptimeMillis
+        return diff < viewConfiguration.doubleTapTimeoutMillis
+    }
+
+    fun positionIsTolerable(prevClick: PointerInputChange, newClick: PointerInputChange): Boolean {
+        val diff = newClick.position - prevClick.position
+        return diff.getDistance() < ClicksSlop
+    }
+}
+
+internal suspend fun PointerInputScope.mouseSelectionDetector(
+    observer: MouseSelectionObserver
+) {
+    awaitEachGesture {
+        val clicksCounter = ClicksCounter(viewConfiguration)
+        while (true) {
+            val down: PointerEvent = awaitMouseEventDown()
+            clicksCounter.update(down)
+            val downChange = down.changes[0]
+            if (down.keyboardModifiers.isShiftPressed) {
+                val started = observer.onExtend(downChange.position)
+                if (started) {
+                    downChange.consume()
+                    drag(downChange.id) {
+                        if (observer.onExtendDrag(it.position)) {
+                            it.consume()
+                        }
+                    }
+                }
+            } else {
+                val selectionMode = when (clicksCounter.clicks) {
+                    1 -> SelectionAdjustment.None
+                    2 -> SelectionAdjustment.Word
+                    else -> SelectionAdjustment.Paragraph
+                }
+                val started = observer.onStart(downChange.position, selectionMode)
+                if (started) {
+                    downChange.consume()
+                    drag(downChange.id) {
+                        if (observer.onDrag(it.position, selectionMode)) {
+                            it.consume()
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+private suspend fun AwaitPointerEventScope.awaitMouseEventDown(): PointerEvent {
+    var event: PointerEvent
+    do {
+        event = awaitPointerEvent(PointerEventPass.Main)
+    } while (
+        !(
+            event.buttons.isPrimaryPressed && event.changes.fastAll {
+                it.type == PointerType.Mouse && it.changedToDown()
+            }
+            )
+    )
+    return event
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..c616ac9
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/MinMaxLinesCoercer.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.newtext.text.modifiers
+
+import androidx.compose.ui.text.Paragraph
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.resolveDefaults
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import kotlin.math.roundToInt
+
+internal class MinMaxLinesCoercer private constructor(
+    val layoutDirection: LayoutDirection,
+    val inputTextStyle: TextStyle,
+    val density: Density,
+    val fontFamilyResolver: FontFamily.Resolver
+) {
+    private val resolvedStyle = resolveDefaults(inputTextStyle, layoutDirection)
+    private var lineHeightCache: Float = Float.NaN
+    private var oneLineHeightCache: Float = Float.NaN
+
+    companion object {
+        // LRU cache of one since this tends to be used for similar styles
+        // ... it may be useful to increase this cache if requested by some dev use case
+        private var last: MinMaxLinesCoercer? = null
+
+        /**
+         * Returns a coercer (possibly cached) with these parameters
+         */
+        fun from(
+            minMaxUtil: MinMaxLinesCoercer?,
+            layoutDirection: LayoutDirection,
+            paramStyle: TextStyle,
+            density: Density,
+            fontFamilyResolver: FontFamily.Resolver
+        ): MinMaxLinesCoercer {
+            minMaxUtil?.let {
+                if (layoutDirection == it.layoutDirection &&
+                    paramStyle == it.inputTextStyle &&
+                    density.density == it.density.density &&
+                    fontFamilyResolver === it.fontFamilyResolver) {
+                    return it
+                }
+            }
+            last?.let {
+                if (layoutDirection == it.layoutDirection &&
+                    paramStyle == it.inputTextStyle &&
+                    density.density == it.density.density &&
+                    fontFamilyResolver === it.fontFamilyResolver) {
+                    return it
+                }
+            }
+            return MinMaxLinesCoercer(
+                layoutDirection,
+                resolveDefaults(paramStyle, layoutDirection),
+                density,
+                fontFamilyResolver
+            ).also {
+                last = it
+            }
+        }
+    }
+
+    /**
+     * Coerce inConstraints to have min and max lines applied.
+     *
+     * On first invocation this will cause (2) Paragraph measurements.
+     */
+    internal fun coerceMaxMinLines(
+        inConstraints: Constraints,
+        minLines: Int,
+        maxLines: Int,
+    ): Constraints {
+        var oneLineHeight = oneLineHeightCache
+        var lineHeight = lineHeightCache
+        if (oneLineHeight.isNaN() || lineHeight.isNaN()) {
+            oneLineHeight = Paragraph(
+                text = EmptyTextReplacement,
+                style = resolvedStyle,
+                constraints = Constraints(),
+                density = density,
+                fontFamilyResolver = fontFamilyResolver,
+                maxLines = 1,
+                ellipsis = false
+            ).height
+
+            val twoLineHeight = Paragraph(
+                text = TwoLineTextReplacement,
+                style = resolvedStyle,
+                constraints = Constraints(),
+                density = density,
+                fontFamilyResolver = fontFamilyResolver,
+                maxLines = 2,
+                ellipsis = false
+            ).height
+
+            lineHeight = twoLineHeight - oneLineHeight
+            oneLineHeightCache = oneLineHeight
+            lineHeightCache = lineHeight
+        }
+        val maxHeight = if (maxLines != Int.MAX_VALUE) {
+            (oneLineHeight + (lineHeight * (maxLines - 1)))
+                .roundToInt()
+                .coerceAtLeast(0)
+        } else {
+            inConstraints.maxHeight
+        }
+        val minHeight = if (minLines != 1) {
+            (oneLineHeight + (lineHeight * (minLines - 1)))
+                .roundToInt()
+                .coerceAtLeast(0)
+                .coerceAtMost(maxHeight)
+        } else {
+            inConstraints.minHeight
+        }
+        return Constraints(
+            minHeight = minHeight,
+            maxHeight = maxHeight,
+            minWidth = inConstraints.minWidth,
+            maxWidth = inConstraints.maxWidth,
+        )
+    }
+}
+
+private const val DefaultWidthCharCount = 10 // min width for TextField is 10 chars long
+private val EmptyTextReplacement = "H".repeat(DefaultWidthCharCount) // just a reference character.
+private val TwoLineTextReplacement = EmptyTextReplacement + "\n" + EmptyTextReplacement
+
+internal fun validateMinMaxLines(minLines: Int, maxLines: Int) {
+    require(minLines > 0 && maxLines > 0) {
+        "both minLines $minLines and maxLines $maxLines must be greater than zero"
+    }
+    require(minLines <= maxLines) {
+        "minLines $minLines must be less than or equal to maxLines $maxLines"
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/MinMaxLinesUtils.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/MinMaxLinesUtils.kt
deleted file mode 100644
index 8310af8..0000000
--- a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/MinMaxLinesUtils.kt
+++ /dev/null
@@ -1,92 +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.ui.text.Paragraph
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontFamily
-import androidx.compose.ui.text.resolveDefaults
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.LayoutDirection
-import kotlin.math.roundToInt
-
-internal fun Constraints.coerceMaxMinLines(
-    layoutDirection: LayoutDirection,
-    minLines: Int,
-    maxLines: Int,
-    paramStyle: TextStyle,
-    density: Density,
-    fontFamilyResolver: FontFamily.Resolver,
-): Constraints {
-    val style = resolveDefaults(paramStyle, layoutDirection)
-    val oneLine = Paragraph(
-        text = EmptyTextReplacement,
-        style = style,
-        constraints = Constraints(),
-        density = density,
-        fontFamilyResolver = fontFamilyResolver,
-        maxLines = 1,
-        ellipsis = false
-    ).height
-
-    val twoLines = Paragraph(
-        text = TwoLineTextReplacement,
-        style = style,
-        constraints = Constraints(),
-        density = density,
-        fontFamilyResolver = fontFamilyResolver,
-        maxLines = 2,
-        ellipsis = false
-    ).height
-
-    val lineHeight = twoLines - oneLine
-    val maxHeight = if (maxLines != Int.MAX_VALUE) {
-        (oneLine + (lineHeight * (maxLines - 1)))
-            .roundToInt()
-            .coerceAtLeast(0)
-    } else {
-        this.maxHeight
-    }
-    val minHeight = if (minLines != 1) {
-        (oneLine + (lineHeight * (minLines - 1)))
-            .roundToInt()
-            .coerceAtLeast(0)
-            .coerceAtMost(maxHeight)
-    } else {
-        this.minHeight
-    }
-    return Constraints(
-        minHeight = minHeight,
-        maxHeight = maxHeight,
-        minWidth = minWidth,
-        maxWidth = maxWidth,
-    )
-}
-
-private const val DefaultWidthCharCount = 10 // min width for TextField is 10 chars long
-private val EmptyTextReplacement = "H".repeat(DefaultWidthCharCount) // just a reference character.
-private val TwoLineTextReplacement = EmptyTextReplacement + "\n" + EmptyTextReplacement
-
-internal fun validateMinMaxLines(minLines: Int, maxLines: Int) {
-    require(minLines > 0 && maxLines > 0) {
-        "both minLines $minLines and maxLines $maxLines must be greater than zero"
-    }
-    require(minLines <= maxLines) {
-        "minLines $minLines must be less than or equal to maxLines $maxLines"
-    }
-}
\ No newline at end of file
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 544e608..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,14 +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.AnnotatedString
-import androidx.compose.ui.text.ExperimentalTextApi
 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
@@ -34,17 +36,38 @@
 import androidx.compose.ui.unit.constrain
 
 internal class MultiParagraphLayoutCache(
-    private val params: TextInlineContentLayoutDrawParams,
-    private val density: Density,
-    private val placeholders: List<AnnotatedString.Range<Placeholder>> = emptyList()
+    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
@@ -76,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
@@ -88,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 {
@@ -109,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
@@ -132,92 +269,20 @@
             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 = placeholders
-            )
-        } 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) {
-            constraints.coerceMaxMinLines(
-                layoutDirection = layoutDirection,
-                minLines = params.minLines,
-                maxLines = params.maxLines,
-                paramStyle = params.style,
-                density = density,
-                fontFamilyResolver = params.fontFamilyResolver
-            )
-        } 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,
-                placeholders,
-                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 {
         // no layout yet
         if (this == null) return true
 
+        // async typeface changes
+        if (this.multiParagraph.intrinsics.hasStaleResolvedFonts) return true
+
         // layout direction changed
         if (layoutDirection != layoutInput.layoutDirection) return true
 
@@ -226,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
@@ -265,7 +330,7 @@
         return false
     }
 
-    private fun TextInlineContentLayoutDrawParams.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) {
@@ -280,24 +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 equalForLayout(value: TextInlineContentLayoutDrawParams): Boolean {
-        return params.equalForLayout(value)
-    }
-
-    fun equalForCallbacks(value: TextInlineContentLayoutDrawParams): Boolean {
-        return params.equalForCallbacks(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/StaticTextModifier.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/StaticTextModifier.kt
deleted file mode 100644
index caff10e..0000000
--- a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/StaticTextModifier.kt
+++ /dev/null
@@ -1,109 +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.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.graphics.drawscope.ContentDrawScope
-import androidx.compose.ui.layout.IntrinsicMeasurable
-import androidx.compose.ui.layout.IntrinsicMeasureScope
-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.LayoutModifierNode
-import androidx.compose.ui.node.SemanticsModifierNode
-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.unit.Constraints
-
-@OptIn(ExperimentalComposeUiApi::class)
-internal class StaticTextModifier(
-    params: TextInlineContentLayoutDrawParams
-) : DelegatingNode(), LayoutModifierNode, DrawModifierNode, SemanticsModifierNode {
-
-    private val drawLayout = delegated { TextInlineContentLayoutDrawModifier(params) }
-
-    private var _semanticsConfiguration: SemanticsConfiguration = generateSemantics(params.text)
-
-    private val semanticsTextLayoutResult: (MutableList<TextLayoutResult>) -> Boolean =
-        { textLayoutResult ->
-            val layout = drawLayout.layoutOrNull?.also {
-                textLayoutResult.add(it)
-            }
-            layout != null
-        }
-
-    private fun generateSemantics(text: AnnotatedString): SemanticsConfiguration {
-        return SemanticsConfiguration().also {
-            it.isMergingSemanticsOfDescendants = false
-            it.isClearingSemantics = false
-            it.text = text
-            it.getTextLayoutResult(action = semanticsTextLayoutResult)
-        }
-    }
-
-    override val semanticsConfiguration: SemanticsConfiguration
-        get() = _semanticsConfiguration
-
-    fun update(params: TextInlineContentLayoutDrawParams) {
-        _semanticsConfiguration = generateSemantics(params.text)
-        drawLayout.params = params
-    }
-
-    override fun IntrinsicMeasureScope.minIntrinsicWidth(
-        measurable: IntrinsicMeasurable,
-        height: Int
-    ): Int {
-        return drawLayout.minIntrinsicWidthNonExtension(this, measurable, height)
-    }
-
-    override fun IntrinsicMeasureScope.minIntrinsicHeight(
-        measurable: IntrinsicMeasurable,
-        width: Int
-    ): Int {
-        return drawLayout.minIntrinsicHeightNonExtension(this, measurable, width)
-    }
-
-    override fun IntrinsicMeasureScope.maxIntrinsicWidth(
-        measurable: IntrinsicMeasurable,
-        height: Int
-    ): Int {
-        return drawLayout.maxIntrinsicWidthNonExtension(this, measurable, height)
-    }
-
-    override fun IntrinsicMeasureScope.maxIntrinsicHeight(
-        measurable: IntrinsicMeasurable,
-        width: Int
-    ): Int {
-        return drawLayout.maxIntrinsicHeightNonExtension(this, measurable, width)
-    }
-
-    override fun MeasureScope.measure(
-        measurable: Measurable,
-        constraints: Constraints
-    ): MeasureResult {
-        return drawLayout.measureNonExtension(this, measurable, constraints)
-    }
-
-    override fun ContentDrawScope.draw() {
-        drawLayout.drawNonExtension(this)
-    }
-}
\ No newline at end of file
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/TextInlineContentLayoutDrawModifier.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextInlineContentLayoutDrawModifier.kt
deleted file mode 100644
index 1ff854e..0000000
--- a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextInlineContentLayoutDrawModifier.kt
+++ /dev/null
@@ -1,193 +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.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.invalidateDraw
-import androidx.compose.ui.node.invalidateLayout
-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 [TextInlineContentLayoutDrawParams]
- */
-@OptIn(ExperimentalComposeUiApi::class)
-internal class TextInlineContentLayoutDrawModifier(
-    params: TextInlineContentLayoutDrawParams
-) : Modifier.Node(), LayoutModifierNode, DrawModifierNode {
-    private var layoutCache: MultiParagraphLayoutCache? = null
-    private var textDelegateDirty = true
-
-    val layoutOrNull: TextLayoutResult?
-        get() = layoutCache?.layoutOrNull
-
-    internal var params: TextInlineContentLayoutDrawParams = params
-        set(value) {
-            validate(params)
-            layoutCache?.let { cache ->
-                if (cache.equalForLayout(value) || cache.equalForCallbacks(value)) {
-                    textDelegateDirty = true
-                    invalidateLayout()
-                }
-            }
-            field = value
-            // if we set params, always redraw.
-            invalidateDraw()
-        }
-
-    private fun validate(params: TextInlineContentLayoutDrawParams) {
-        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
-
-        if (didChangeLayout) {
-            invalidateDraw()
-            params.onTextLayout?.invoke(textLayoutResult)
-        }
-
-        // 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,
-            mapOf(
-                FirstBaseline to textLayoutResult.firstBaseline.roundToInt(),
-                LastBaseline to textLayoutResult.lastBaseline.roundToInt()
-            )
-        ) {
-            // this is basically a graphicsLayer
-            placeable.placeWithLayer(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() {
-        drawIntoCanvas { canvas ->
-            TextPainter.paint(canvas, requireNotNull(layoutCache?.layout))
-        }
-        if (!params.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/TextInlineContentLayoutDrawParams.kt b/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextInlineContentLayoutDrawParams.kt
deleted file mode 100644
index fa3bbeb..0000000
--- a/compose/foundation/foundation-newtext/src/commonMain/kotlin/androidx/compose/foundation/newtext/text/modifiers/TextInlineContentLayoutDrawParams.kt
+++ /dev/null
@@ -1,68 +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
-
-// 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 TextInlineContentLayoutDrawParams(
-    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,
-) {
-    init {
-        validateMinMaxLines(minLines, maxLines)
-    }
-}
-
-internal fun TextInlineContentLayoutDrawParams.equalForCallbacks(
-    newParams: TextInlineContentLayoutDrawParams
-): Boolean {
-    return onTextLayout == newParams.onTextLayout &&
-        onPlaceholderLayout == newParams.onPlaceholderLayout
-}
-internal fun TextInlineContentLayoutDrawParams.equalForLayout(
-    newParams: TextInlineContentLayoutDrawParams
-): Boolean {
-    if (this === newParams) return true
-
-    if (text != newParams.text) return false
-    if (!style.hasSameLayoutAffectingAttributes(newParams.style)) return false
-    if (maxLines != newParams.maxLines) return false
-    if (minLines != newParams.minLines) return false
-    if (softWrap != newParams.softWrap) return false
-    if (fontFamilyResolver != newParams.fontFamilyResolver) return false
-    if (overflow != newParams.overflow) return false
-    if (placeholders != newParams.placeholders) return false
-
-    return true
-}
\ 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/desktopMain/kotlin/androidx/compose/foundation/BasicContextMenuRepresentation.desktop.kt b/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/BasicContextMenuRepresentation.desktop.kt
new file mode 100644
index 0000000..86ddbd5
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/BasicContextMenuRepresentation.desktop.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.sizeIn
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Popup
+import androidx.compose.ui.window.rememberCursorPositionProvider
+
+// Design of basic represenation is from Material specs:
+// https://material.io/design/interaction/states.html#hover
+// https://material.io/components/menus#specs
+
+val LightDefaultContextMenuRepresentation = DefaultContextMenuRepresentation(
+    backgroundColor = Color.White,
+    textColor = Color.Black,
+    itemHoverColor = Color.Black.copy(alpha = 0.04f)
+)
+
+val DarkDefaultContextMenuRepresentation = DefaultContextMenuRepresentation(
+    backgroundColor = Color(0xFF121212), // like surface in darkColors
+    textColor = Color.White,
+    itemHoverColor = Color.White.copy(alpha = 0.04f)
+)
+
+class DefaultContextMenuRepresentation(
+    private val backgroundColor: Color,
+    private val textColor: Color,
+    private val itemHoverColor: Color
+) : ContextMenuRepresentation {
+    @Composable
+    override fun Representation(state: ContextMenuState, items: List<ContextMenuItem>) {
+        val isOpen = state.status is ContextMenuState.Status.Open
+        if (isOpen) {
+            Popup(
+                focusable = true,
+                onDismissRequest = { state.status = ContextMenuState.Status.Closed },
+                popupPositionProvider = rememberCursorPositionProvider()
+            ) {
+                Column(
+                    modifier = Modifier
+                        .shadow(8.dp)
+                        .background(backgroundColor)
+                        .padding(vertical = 4.dp)
+                        .width(IntrinsicSize.Max)
+                        .verticalScroll(rememberScrollState())
+
+                ) {
+                    items.distinctBy { it.label }.forEach { item ->
+                        MenuItemContent(
+                            itemHoverColor = itemHoverColor,
+                            onClick = {
+                                state.status = ContextMenuState.Status.Closed
+                                item.onClick()
+                            }
+                        ) {
+                            BasicText(text = item.label, style = TextStyle(color = textColor))
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun MenuItemContent(
+    itemHoverColor: Color,
+    onClick: () -> Unit,
+    content: @Composable RowScope.() -> Unit
+) {
+    var hovered by remember { mutableStateOf(false) }
+    Row(
+        modifier = Modifier
+            .clickable(
+                onClick = onClick,
+            )
+            .onHover { hovered = it }
+            .background(if (hovered) itemHoverColor else Color.Transparent)
+            .fillMaxWidth()
+            // Preferred min and max width used during the intrinsic measurement.
+            .sizeIn(
+                minWidth = 112.dp,
+                maxWidth = 280.dp,
+                minHeight = 32.dp
+            )
+            .padding(
+                PaddingValues(
+                    horizontal = 16.dp,
+                    vertical = 0.dp
+                )
+            ),
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        content()
+    }
+}
+
+private fun Modifier.onHover(onHover: (Boolean) -> Unit) = pointerInput(Unit) {
+    awaitPointerEventScope {
+        while (true) {
+            val event = awaitPointerEvent()
+            when (event.type) {
+                PointerEventType.Enter -> onHover(true)
+                PointerEventType.Exit -> onHover(false)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/ContextMenuProvider.desktop.kt b/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/ContextMenuProvider.desktop.kt
new file mode 100644
index 0000000..2a7fa89
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/ContextMenuProvider.desktop.kt
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation
+
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.ProvidableCompositionLocal
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.input.pointer.AwaitPointerEventScope
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.changedToDown
+import androidx.compose.ui.input.pointer.isSecondaryPressed
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.util.fastAll
+
+/**
+ * Defines a container where context menu is available. Menu is triggered by right mouse clicks.
+ * Representation of menu is defined by [LocalContextMenuRepresentation]`
+ *
+ * @param items List of context menu items. Final context menu contains all items from descendant
+ * [ContextMenuArea] and [ContextMenuDataProvider].
+ * @param state [ContextMenuState] of menu controlled by this area.
+ * @param enabled If false then gesture detector is disabled.
+ * @param content The content of the [ContextMenuArea].
+ */
+@Composable
+fun ContextMenuArea(
+    items: () -> List<ContextMenuItem>,
+    state: ContextMenuState = remember { ContextMenuState() },
+    enabled: Boolean = true,
+    content: @Composable () -> Unit
+) {
+    val data = ContextMenuData(items, LocalContextMenuData.current)
+
+    ContextMenuDataProvider(data) {
+        Box(Modifier.contextMenuDetector(state, enabled), propagateMinConstraints = true) {
+            content()
+        }
+        LocalContextMenuRepresentation.current.Representation(state, data.allItems)
+    }
+}
+
+/**
+ * Adds items to the hierarchy of context menu items. Can be used, for example, to customize
+ * context menu of text fields.
+ *
+ * @param items List of context menu items. Final context menu contains all items from descendant
+ * [ContextMenuArea] and [ContextMenuDataProvider].
+ * @param content The content of the [ContextMenuDataProvider].
+ *
+ * @see [[ContextMenuArea]]
+ */
+@Composable
+fun ContextMenuDataProvider(
+    items: () -> List<ContextMenuItem>,
+    content: @Composable () -> Unit
+) {
+    ContextMenuDataProvider(
+        ContextMenuData(items, LocalContextMenuData.current),
+        content
+    )
+}
+
+@Composable
+internal fun ContextMenuDataProvider(
+    data: ContextMenuData,
+    content: @Composable () -> Unit
+) {
+    CompositionLocalProvider(
+        LocalContextMenuData provides data
+    ) {
+        content()
+    }
+}
+
+private val LocalContextMenuData = staticCompositionLocalOf<ContextMenuData?> {
+    null
+}
+
+private fun Modifier.contextMenuDetector(
+    state: ContextMenuState,
+    enabled: Boolean = true
+): Modifier {
+    return if (
+        enabled && state.status == ContextMenuState.Status.Closed
+    ) {
+        this.pointerInput(state) {
+            awaitEachGesture {
+                val event = awaitEventFirstDown()
+                if (event.buttons.isSecondaryPressed) {
+                    event.changes.forEach { it.consume() }
+                    state.status =
+                        ContextMenuState.Status.Open(Rect(event.changes[0].position, 0f))
+                }
+            }
+        }
+    } else {
+        Modifier
+    }
+}
+
+private suspend fun AwaitPointerEventScope.awaitEventFirstDown(): PointerEvent {
+    var event: PointerEvent
+    do {
+        event = awaitPointerEvent()
+    } while (
+        !event.changes.fastAll { it.changedToDown() }
+    )
+    return event
+}
+
+/**
+ * Individual element of context menu.
+ *
+ * @param label The text to be displayed as a context menu item.
+ * @param onClick The action to be executed after click on the item.
+ */
+class ContextMenuItem(
+    val label: String,
+    val onClick: () -> Unit
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+
+        other as ContextMenuItem
+
+        if (label != other.label) return false
+        if (onClick != other.onClick) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = label.hashCode()
+        result = 31 * result + onClick.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "ContextMenuItem(label='$label')"
+    }
+}
+
+/**
+ * Data container contains all [ContextMenuItem]s were defined previously in the hierarchy.
+ * [ContextMenuRepresentation] uses it to display context menu.
+ */
+class ContextMenuData(
+    val items: () -> List<ContextMenuItem>,
+    val next: ContextMenuData?
+) {
+
+    internal val allItems: List<ContextMenuItem> by lazy {
+        allItemsSeq.toList()
+    }
+
+    internal val allItemsSeq: Sequence<ContextMenuItem>
+        get() = sequence {
+            yieldAll(items())
+            next?.let { yieldAll(it.allItemsSeq) }
+        }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+
+        other as ContextMenuData
+
+        if (items != other.items) return false
+        if (next != other.next) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = items.hashCode()
+        result = 31 * result + (next?.hashCode() ?: 0)
+        return result
+    }
+}
+
+/**
+ * Represents a state of context menu in [ContextMenuArea]. [status] is implemented
+ * via [androidx.compose.runtime.MutableState] so it's possible to track it inside @Composable
+ * functions.
+ */
+class ContextMenuState {
+    sealed class Status {
+        class Open(
+            val rect: Rect
+        ) : Status() {
+            override fun equals(other: Any?): Boolean {
+                if (this === other) return true
+                if (other == null || this::class != other::class) return false
+
+                other as Open
+
+                if (rect != other.rect) return false
+
+                return true
+            }
+
+            override fun hashCode(): Int {
+                return rect.hashCode()
+            }
+
+            override fun toString(): String {
+                return "Open(rect=$rect)"
+            }
+        }
+
+        object Closed : Status()
+    }
+
+    var status: Status by mutableStateOf(Status.Closed)
+}
+
+/**
+ * Implementations of this interface are responsible for displaying context menus. There are two
+ * implementations out of the box: [LightDefaultContextMenuRepresentation] and
+ * [DarkDefaultContextMenuRepresentation].
+ * To change currently used representation, different value for [LocalContextMenuRepresentation]
+ * could be provided.
+ */
+interface ContextMenuRepresentation {
+    @Composable
+    fun Representation(state: ContextMenuState, items: List<ContextMenuItem>)
+}
+
+/**
+ * Composition local that keeps [ContextMenuRepresentation] which is used by [ContextMenuArea]s.
+ */
+val LocalContextMenuRepresentation:
+    ProvidableCompositionLocal<ContextMenuRepresentation> = staticCompositionLocalOf {
+    LightDefaultContextMenuRepresentation
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/ContextMenu.desktop.kt b/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/ContextMenu.desktop.kt
new file mode 100644
index 0000000..dcea9d1
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/ContextMenu.desktop.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.newtext.text.copypasta
+
+import androidx.compose.foundation.ContextMenuItem
+import androidx.compose.foundation.ContextMenuState
+import androidx.compose.foundation.newtext.text.copypasta.selection.SelectionManager
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.geometry.Offset
+import kotlinx.coroutines.flow.collect
+
+@Composable
+internal actual fun ContextMenuArea(
+    manager: SelectionManager,
+    content: @Composable () -> Unit
+) {
+    /* noop*/
+}
+
+@Composable
+internal fun OpenMenuAdjuster(state: ContextMenuState, adjustAction: (Offset) -> Unit) {
+    LaunchedEffect(state) {
+        snapshotFlow { state.status }.collect { status ->
+            if (status is ContextMenuState.Status.Open) {
+                adjustAction(status.rect.center)
+            }
+        }
+    }
+}
+
+@Composable
+internal fun SelectionManager.contextMenuItems(): () -> List<ContextMenuItem> {
+    return {
+        listOf()
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/StringHelpers.desktop.kt b/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/StringHelpers.desktop.kt
new file mode 100644
index 0000000..58ac4c9
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/StringHelpers.desktop.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.compose.foundation.newtext.text.copypasta
+
+import org.jetbrains.skia.BreakIterator
+
+internal actual fun String.findPrecedingBreak(index: Int): Int {
+    val it = BreakIterator.makeCharacterInstance()
+    it.setText(this)
+    return it.preceding(index)
+}
+
+internal actual fun String.findFollowingBreak(index: Int): Int {
+    val it = BreakIterator.makeCharacterInstance()
+    it.setText(this)
+    return it.following(index)
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/TextPointerIcon.desktop.kt b/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/TextPointerIcon.desktop.kt
new file mode 100644
index 0000000..ff55d6d
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/TextPointerIcon.desktop.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.newtext.text.copypasta
+
+import androidx.compose.ui.input.pointer.PointerIcon
+import java.awt.Cursor
+
+internal actual val textPointerIcon: PointerIcon =
+    PointerIcon(Cursor(Cursor.TEXT_CURSOR))
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/DesktopSelectionHandles.desktop.kt b/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/DesktopSelectionHandles.desktop.kt
new file mode 100644
index 0000000..c509e01
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/DesktopSelectionHandles.desktop.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.newtext.text.copypasta.selection
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.text.style.ResolvedTextDirection
+
+@Composable
+internal actual fun SelectionHandle(
+    position: Offset,
+    isStartHandle: Boolean,
+    direction: ResolvedTextDirection,
+    handlesCrossed: Boolean,
+    modifier: Modifier,
+    content: (@Composable () -> Unit)?
+) {
+    // TODO
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionManager.desktop.kt b/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionManager.desktop.kt
new file mode 100644
index 0000000..a03574a
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/desktopMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/selection/SelectionManager.desktop.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.newtext.text.copypasta.selection
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.KeyEvent
+
+// this doesn't sounds very sustainable
+// it would end up being a function for any conceptual keyevent (selectall, cut, copy, paste)
+// TODO(b/1564937)
+internal actual fun isCopyKeyEvent(keyEvent: KeyEvent) = true
+
+/**
+ * Magnification is not supported on desktop.
+ */
+internal actual fun Modifier.selectionMagnifier(manager: SelectionManager): Modifier = this
diff --git a/compose/foundation/foundation-newtext/src/jvmMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/AtomicLong.kt b/compose/foundation/foundation-newtext/src/jvmMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/AtomicLong.kt
new file mode 100644
index 0000000..8f2bd49
--- /dev/null
+++ b/compose/foundation/foundation-newtext/src/jvmMain/kotlin/androidx/compose/foundation/newtext/text/copypasta/AtomicLong.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// ktlint-disable filename
+
+package androidx.compose.foundation.newtext.text.copypasta
+
+internal actual typealias AtomicLong = java.util.concurrent.atomic.AtomicLong
\ No newline at end of file
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 40015b8..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(
-            TextInlineContentLayoutDrawParams(
-                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(
-            TextInlineContentLayoutDrawParams(
-                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/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..aa4b819 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;
@@ -718,6 +719,9 @@
   public final class LazyNearestItemsRangeKt {
   }
 
+  public final class LazyPinnableContainerProviderKt {
+  }
+
   public final class LazySaveableStateHolderKt {
   }
 
@@ -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..8ffe086 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;
@@ -876,6 +875,9 @@
     method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<kotlin.ranges.IntRange> rememberLazyNearestItemsRangeState(kotlin.jvm.functions.Function0<java.lang.Integer> firstVisibleItemIndex, kotlin.jvm.functions.Function0<java.lang.Integer> slidingWindowSize, kotlin.jvm.functions.Function0<java.lang.Integer> extraItemCount);
   }
 
+  public final class LazyPinnableContainerProviderKt {
+  }
+
   public final class LazySaveableStateHolderKt {
   }
 
@@ -902,10 +904,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 +935,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 +944,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 +963,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 +1014,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 +1275,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 +1366,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..aa4b819 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;
@@ -718,6 +719,9 @@
   public final class LazyNearestItemsRangeKt {
   }
 
+  public final class LazyPinnableContainerProviderKt {
+  }
+
   public final class LazySaveableStateHolderKt {
   }
 
@@ -1014,6 +1018,9 @@
   public final class LongPressTextDragObserverKt {
   }
 
+  public final class PointerMoveDetectorKt {
+  }
+
   public final class StringHelpersKt {
   }
 
diff --git a/compose/foundation/foundation/benchmark/build.gradle b/compose/foundation/foundation/benchmark/build.gradle
index 2b61744..905f5b4 100644
--- a/compose/foundation/foundation/benchmark/build.gradle
+++ b/compose/foundation/foundation/benchmark/build.gradle
@@ -31,6 +31,7 @@
     androidTestImplementation project(":compose:ui:ui-text:ui-text-benchmark")
     androidTestImplementation project(":compose:foundation:foundation-layout")
     androidTestImplementation project(":compose:foundation:foundation")
+    androidTestImplementation project(":compose:foundation:foundation-do-not-ship-newtext")
     androidTestImplementation project(":compose:material:material")
     androidTestImplementation project(":compose:benchmark-utils")
     androidTestImplementation(libs.testRules)
@@ -43,6 +44,10 @@
 
 android {
     namespace "androidx.compose.foundation.benchmark"
+    defaultConfig {
+        // must be one of: 'None', 'StackSampling', or 'MethodTracing'
+        testInstrumentationRunnerArguments["androidx.benchmark.profiling.mode"]= 'None'
+    }
 }
 
 androidx {
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/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/empirical/IfNotEmptyCallText.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/empirical/IfNotEmptyCallText.kt
index 650c032..e19ae6b 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/empirical/IfNotEmptyCallText.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/empirical/IfNotEmptyCallText.kt
@@ -16,7 +16,7 @@
 
 package androidx.compose.foundation.benchmark.text.empirical
 
-import androidx.compose.material.Text
+import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.testutils.LayeredComposeTestCase
@@ -24,6 +24,7 @@
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
 import androidx.compose.testutils.benchmark.toggleStateBenchmarkComposeMeasureLayout
 import androidx.compose.testutils.benchmark.toggleStateBenchmarkRecompose
+import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontFamily
 import androidx.test.filters.LargeTest
 import org.junit.Rule
@@ -39,10 +40,15 @@
 class IfNotEmptyCallText(private val text: String) : LayeredComposeTestCase(), ToggleableTestCase {
     private var toggleText = mutableStateOf("")
 
+    private val style = TextStyle.Default.copy(fontFamily = FontFamily.Monospace)
+
     @Composable
     override fun MeasuredContent() {
         if (toggleText.value.isNotEmpty()) {
-            Text(toggleText.value, fontFamily = FontFamily.Monospace)
+            BasicText(
+                toggleText.value,
+                style = style
+            )
         }
     }
 
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/empirical/ModifierIfNotEmptyCallText.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/empirical/ModifierIfNotEmptyCallText.kt
new file mode 100644
index 0000000..18424f4
--- /dev/null
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/empirical/ModifierIfNotEmptyCallText.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.compose.foundation.benchmark.text.empirical
+
+import androidx.compose.foundation.newtext.text.TextUsingModifier
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ToggleableTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.toggleStateBenchmarkComposeMeasureLayout
+import androidx.compose.testutils.benchmark.toggleStateBenchmarkRecompose
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Toggle between missing Text and Text("aaa..") to simulate backend text loading.
+ *
+ * This intentionally hits as many text caches as possible, to isolate compose setText behavior.
+ */
+@OptIn(ExperimentalTextApi::class)
+class ModifierIfNotEmptyCallText(
+    private val text: String
+) : LayeredComposeTestCase(), ToggleableTestCase {
+    private var toggleText = mutableStateOf("")
+
+    private val style = TextStyle.Default.copy(fontFamily = FontFamily.Monospace)
+
+    @Composable
+    override fun MeasuredContent() {
+        if (toggleText.value.isNotEmpty()) {
+            TextUsingModifier(
+                text = toggleText.value,
+                style = style
+            )
+        }
+    }
+
+    override fun toggleState() {
+        if (toggleText.value == "") {
+            toggleText.value = text
+        } else {
+            toggleText.value = ""
+        }
+    }
+}
+
+@LargeTest
+@RunWith(Parameterized::class)
+open class ModifierIfNotEmptyParent(private val size: Int) {
+
+    @get:Rule
+    val benchmarkRule = ComposeBenchmarkRule()
+
+    private val caseFactory = {
+        val text = generateCacheableStringOf(size)
+        ModifierIfNotEmptyCallText(text)
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "size={0}")
+        fun initParameters(): Array<Any> = arrayOf()
+    }
+
+    @Test
+    fun recomposeOnly() {
+        benchmarkRule.toggleStateBenchmarkRecompose(caseFactory)
+    }
+
+    @Test
+    fun recomposeMeasureLayout() {
+        benchmarkRule.toggleStateBenchmarkComposeMeasureLayout(caseFactory)
+    }
+}
+
+/**
+ * Metrics determined from all apps
+ */
+@LargeTest
+@RunWith(Parameterized::class)
+class ModifierAllAppsIfNotEmptyCallText(size: Int) : ModifierIfNotEmptyParent(size) {
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "size={0}")
+        fun initParameters(): Array<Any> = AllApps.TextLengths
+    }
+}
+
+/**
+ * Metrics for Chat-like apps.
+ *
+ * These apps typically have more longer strings, due to user generated content.
+ */
+@LargeTest
+@RunWith(Parameterized::class)
+class ModifierChatAppIfNotEmptyCallText(size: Int) : ModifierIfNotEmptyParent(size) {
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "size={0}")
+        fun initParameters(): Array<Any> = ChatApps.TextLengths
+    }
+}
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/empirical/ModifierSetText.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/empirical/ModifierSetText.kt
new file mode 100644
index 0000000..e460323
--- /dev/null
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/empirical/ModifierSetText.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.compose.foundation.benchmark.text.empirical
+
+import androidx.compose.foundation.newtext.text.TextUsingModifier
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.ToggleableTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.toggleStateBenchmarkComposeMeasureLayout
+import androidx.compose.testutils.benchmark.toggleStateBenchmarkRecompose
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * Toggle between "" and "aaaa..." to simulate backend text loading.
+ *
+ * This intentionally hits as many text caches as possible, to isolate compose setText behavior.
+ */
+class ModifierSetText(private val text: String) : LayeredComposeTestCase(), ToggleableTestCase {
+    private val toggleText = mutableStateOf("")
+
+    private val style = TextStyle.Default.copy(fontFamily = FontFamily.Monospace)
+
+    @OptIn(ExperimentalTextApi::class)
+    @Composable
+    override fun MeasuredContent() {
+        TextUsingModifier(
+            toggleText.value,
+            style = style
+        )
+    }
+
+    override fun toggleState() {
+        if (toggleText.value == "") {
+            toggleText.value = text
+        } else {
+            toggleText.value = ""
+        }
+    }
+}
+
+@LargeTest
+@RunWith(Parameterized::class)
+open class ModifierSetTextParent(private val size: Int) {
+
+    @get:Rule
+    val benchmarkRule = ComposeBenchmarkRule()
+
+    private val caseFactory = {
+        val text = generateCacheableStringOf(size)
+        ModifierSetText(text)
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "size={0}")
+        fun initParameters(): Array<Any> = arrayOf()
+    }
+
+    @Test
+    fun recomposeOnly() {
+        benchmarkRule.toggleStateBenchmarkRecompose(caseFactory)
+    }
+
+    @Test
+    fun recomposeMeasureLayout() {
+        benchmarkRule.toggleStateBenchmarkComposeMeasureLayout(caseFactory)
+    }
+}
+
+/**
+ * Metrics determined from all apps
+ */
+@LargeTest
+@RunWith(Parameterized::class)
+class ModifierAllAppsSetText(size: Int) : ModifierSetTextParent(size) {
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "size={0}")
+        fun initParameters(): Array<Any> = AllApps.TextLengths
+    }
+}
+
+/**
+ * Metrics for Chat-like apps.
+ *
+ * These apps typically have more longer strings, due to user generated content.
+ */
+@LargeTest
+@RunWith(Parameterized::class)
+class ModifierChatAppSetText(size: Int) : ModifierSetTextParent(size) {
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "size={0}")
+        fun initParameters(): Array<Any> = ChatApps.TextLengths
+    }
+}
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/empirical/SetText.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/empirical/SetText.kt
index 36cf78a..b82eb69 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/empirical/SetText.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/empirical/SetText.kt
@@ -16,7 +16,7 @@
 
 package androidx.compose.foundation.benchmark.text.empirical
 
-import androidx.compose.material.Text
+import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.testutils.LayeredComposeTestCase
@@ -24,6 +24,7 @@
 import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
 import androidx.compose.testutils.benchmark.toggleStateBenchmarkComposeMeasureLayout
 import androidx.compose.testutils.benchmark.toggleStateBenchmarkRecompose
+import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontFamily
 import androidx.test.filters.LargeTest
 import org.junit.Rule
@@ -39,9 +40,14 @@
 class SetText(private val text: String) : LayeredComposeTestCase(), ToggleableTestCase {
     private var toggleText = mutableStateOf("")
 
+    private val style = TextStyle.Default.copy(fontFamily = FontFamily.Monospace)
+
     @Composable
     override fun MeasuredContent() {
-        Text(toggleText.value, fontFamily = FontFamily.Monospace)
+        BasicText(
+            toggleText.value,
+            style = style
+        )
     }
 
     override fun toggleState() {
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/ComposeText.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
index b4c667c..bf5e4dd 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
@@ -554,7 +554,6 @@
     }
 }
 
-@OptIn(ExperimentalTextApi::class)
 @Composable
 fun TextDemoHyphens() {
     val text = "Transformation"
@@ -562,7 +561,7 @@
         hyphens = Hyphens.Auto)
     val textStyleHyphensOff = TextStyle(fontSize = fontSize8, color = Color.Blue,
         hyphens = Hyphens.None)
-    Column() {
+    Column {
         var width by remember { mutableStateOf(30f) }
         Slider(
             value = width,
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/LineBreakDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LineBreakDemo.kt
new file mode 100644
index 0000000..55ccf56
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LineBreakDemo.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.border
+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.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.Slider
+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.SpanStyle
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.LineBreak
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+private val lineBreakOptions = listOf(
+    "Simple" to LineBreak.Simple,
+    "Paragraph" to LineBreak.Paragraph,
+    "Heading" to LineBreak.Heading,
+    "Custom" to LineBreak(
+        strategy = LineBreak.Strategy.Balanced,
+        strictness = LineBreak.Strictness.Strict,
+        wordBreak = LineBreak.WordBreak.Default
+    )
+)
+
+private val demoText = "This is an example text\n今日は自由が丘で焼き鳥を食べます。"
+private val presetNameStyle = SpanStyle(fontWeight = FontWeight.Bold, fontSize = 16.sp)
+
+@Composable
+fun TextLineBreakDemo() {
+    val selectedFontSize = remember { mutableStateOf(16f) }
+
+    Column(modifier = Modifier.fillMaxSize()) {
+        Text("Font size: ${selectedFontSize.value}")
+        Slider(
+            value = selectedFontSize.value,
+            onValueChange = { value -> selectedFontSize.value = value },
+            valueRange = 8f..48f
+        )
+
+        Row(Modifier.fillMaxWidth()) {
+            val textModifier = Modifier
+                .wrapContentHeight()
+                .padding(horizontal = 5.dp)
+                .border(1.dp, Color.Gray)
+
+            lineBreakOptions.forEach { (presetName, preset) ->
+                Text(
+                    text = buildAnnotatedString {
+                        withStyle(presetNameStyle) {
+                            append(presetName)
+                            append(":\n")
+                        }
+                        append(demoText)
+                    },
+                    style = TextStyle(
+                        lineBreak = preset,
+                        fontSize = selectedFontSize.value.sp
+                    ),
+                    modifier = textModifier.weight(1f)
+                )
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LineBreakingDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LineBreakingDemo.kt
deleted file mode 100644
index 77bf51f..0000000
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LineBreakingDemo.kt
+++ /dev/null
@@ -1,95 +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.demos.text
-
-import androidx.compose.foundation.border
-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.padding
-import androidx.compose.foundation.layout.wrapContentHeight
-import androidx.compose.material.Slider
-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.ExperimentalTextApi
-import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.LineBreak
-import androidx.compose.ui.text.withStyle
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-
-@OptIn(ExperimentalTextApi::class)
-private val lineBreakOptions = listOf(
-    "Simple" to LineBreak.Simple,
-    "Paragraph" to LineBreak.Paragraph,
-    "Heading" to LineBreak.Heading,
-    "Custom" to LineBreak(
-        strategy = LineBreak.Strategy.Balanced,
-        strictness = LineBreak.Strictness.Strict,
-        wordBreak = LineBreak.WordBreak.Default
-    )
-)
-
-private val demoText = "This is an example text\n今日は自由が丘で焼き鳥を食べます。"
-private val presetNameStyle = SpanStyle(fontWeight = FontWeight.Bold, fontSize = 16.sp)
-
-@OptIn(ExperimentalTextApi::class)
-@Composable
-fun TextLineBreakingDemo() {
-    val selectedFontSize = remember { mutableStateOf(16f) }
-
-    Column(modifier = Modifier.fillMaxSize()) {
-        Text("Font size: ${selectedFontSize.value}")
-        Slider(
-            value = selectedFontSize.value,
-            onValueChange = { value -> selectedFontSize.value = value },
-            valueRange = 8f..48f
-        )
-
-        Row(Modifier.fillMaxWidth()) {
-            val textModifier = Modifier
-                .wrapContentHeight()
-                .padding(horizontal = 5.dp)
-                .border(1.dp, Color.Gray)
-
-            lineBreakOptions.forEach { (presetName, preset) ->
-                Text(
-                    text = buildAnnotatedString {
-                        withStyle(presetNameStyle) {
-                            append(presetName)
-                            append(":\n")
-                        }
-                        append(demoText)
-                    },
-                    style = TextStyle(
-                        lineBreak = preset,
-                        fontSize = selectedFontSize.value.sp
-                    ),
-                    modifier = textModifier.weight(1f)
-                )
-            }
-        }
-    }
-}
\ No newline at end of file
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 cf8c80a..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
@@ -38,12 +38,15 @@
                 DemoCategory(
                     "Line breaking",
                     listOf(
-                        ComposableDemo("Line breaking") { TextLineBreakingDemo() },
+                        ComposableDemo("Line Break") { TextLineBreakDemo() },
                         ComposableDemo("Hyphens") { TextDemoHyphens() },
                         ComposableDemo("Ellipsize") { EllipsizeDemo() },
                         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 3f5502b..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
@@ -24,6 +24,7 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.testutils.assertIsEqualTo
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.assertHeightIsEqualTo
 import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
@@ -32,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
 
@@ -118,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
@@ -149,6 +161,8 @@
         )
     }
 
+    internal fun Modifier.debugBorder(color: Color = Color.Black) = border(1.dp, color)
+
     companion object {
         internal const val FrameDuration = 16L
     }
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/ClickableInScrollableViewGroupTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableInScrollableViewGroupTest.kt
index 0ceccea..eb18a04 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableInScrollableViewGroupTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableInScrollableViewGroupTest.kt
@@ -37,7 +37,9 @@
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.launch
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -203,6 +205,10 @@
     /**
      * Test case for a [clickable] inside an [AndroidView] inside a non-scrollable Compose container
      */
+    @Ignore(
+        "b/203141462 - currently this is not implemented so AndroidView()s will always " +
+            "appear scrollable"
+    )
     @Test
     fun clickable_androidViewInNotScrollableContainer() {
         val interactionSource = MutableInteractionSource()
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 d182639..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
@@ -22,6 +22,7 @@
 import androidx.compose.animation.rememberSplineBasedDecay
 import androidx.compose.foundation.gestures.DefaultFlingBehavior
 import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.ModifierLocalScrollableContainer
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.ScrollScope
 import androidx.compose.foundation.gestures.ScrollableDefaults
@@ -59,7 +60,6 @@
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.consumeScrollContainerInfo
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
@@ -69,6 +69,8 @@
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.input.pointer.util.VelocityTracker
 import androidx.compose.ui.materialize
+import androidx.compose.ui.modifier.ModifierLocalConsumer
+import androidx.compose.ui.modifier.ModifierLocalReadScope
 import androidx.compose.ui.platform.AbstractComposeView
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.LocalFocusManager
@@ -1688,73 +1690,49 @@
     fun scrollable_setsModifierLocalScrollableContainer() {
         val controller = ScrollableState { it }
 
-        var isOuterInVerticalScrollableContainer: Boolean? = null
-        var isInnerInVerticalScrollableContainer: Boolean? = null
-        var isOuterInHorizontalScrollableContainer: Boolean? = null
-        var isInnerInHorizontalScrollableContainer: Boolean? = null
+        var isOuterInScrollableContainer: Boolean? = null
+        var isInnerInScrollableContainer: Boolean? = null
         rule.setContent {
             Box {
                 Box(
                     modifier = Modifier
                         .testTag(scrollableBoxTag)
                         .size(100.dp)
-                        .consumeScrollContainerInfo {
-                            isOuterInVerticalScrollableContainer = it?.canScrollVertically()
-                            isOuterInHorizontalScrollableContainer = it?.canScrollHorizontally()
-                        }
+                        .then(
+                            object : ModifierLocalConsumer {
+                                override fun onModifierLocalsUpdated(
+                                    scope: ModifierLocalReadScope
+                                ) {
+                                    with(scope) {
+                                        isOuterInScrollableContainer =
+                                            ModifierLocalScrollableContainer.current
+                                    }
+                                }
+                            }
+                        )
                         .scrollable(
                             state = controller,
                             orientation = Orientation.Horizontal
                         )
-                        .consumeScrollContainerInfo {
-                            isInnerInHorizontalScrollableContainer = it?.canScrollHorizontally()
-                            isInnerInVerticalScrollableContainer = it?.canScrollVertically()
-                        }
+                        .then(
+                            object : ModifierLocalConsumer {
+                                override fun onModifierLocalsUpdated(
+                                    scope: ModifierLocalReadScope
+                                ) {
+                                    with(scope) {
+                                        isInnerInScrollableContainer =
+                                            ModifierLocalScrollableContainer.current
+                                    }
+                                }
+                            }
+                        )
                 )
             }
         }
 
         rule.runOnIdle {
-            assertThat(isInnerInHorizontalScrollableContainer).isTrue()
-            assertThat(isInnerInVerticalScrollableContainer).isFalse()
-            assertThat(isOuterInVerticalScrollableContainer).isFalse()
-            assertThat(isOuterInHorizontalScrollableContainer).isFalse()
-        }
-    }
-
-    @Test
-    fun scrollable_nested_setsModifierLocalScrollableContainer() {
-        val horizontalController = ScrollableState { it }
-        val verticalController = ScrollableState { it }
-
-        var horizontalDrag: Boolean? = null
-        var verticalDrag: Boolean? = null
-        rule.setContent {
-            Box(
-                modifier = Modifier
-                    .size(100.dp)
-                    .scrollable(
-                        state = horizontalController,
-                        orientation = Orientation.Horizontal
-                    )
-            ) {
-                Box(
-                    modifier = Modifier
-                        .size(100.dp)
-                        .scrollable(
-                            state = verticalController,
-                            orientation = Orientation.Vertical
-                        )
-                        .consumeScrollContainerInfo {
-                            horizontalDrag = it?.canScrollHorizontally()
-                            verticalDrag = it?.canScrollVertically()
-                        })
-            }
-        }
-
-        rule.runOnIdle {
-            assertThat(horizontalDrag).isTrue()
-            assertThat(verticalDrag).isTrue()
+            assertThat(isOuterInScrollableContainer).isFalse()
+            assertThat(isInnerInScrollableContainer).isTrue()
         }
     }
 
@@ -2574,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/LazyGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
index 02475c7..24d20dd 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
@@ -23,6 +23,7 @@
 import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.layout.size
@@ -63,9 +64,11 @@
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeDown
 import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.size
@@ -1408,6 +1411,59 @@
             }
         }
     }
+
+    @Test
+    fun changingMaxSpansCount() {
+        val state = LazyGridState()
+        state.prefetchingEnabled = false
+        val itemSizePx = 100
+        val itemSizeDp = with(rule.density) { itemSizePx.toDp() }
+        var expanded by mutableStateOf(true)
+        rule.setContent {
+            Row {
+                LazyGrid(
+                    GridCells.Adaptive(itemSizeDp),
+                    Modifier
+                        .testTag(LazyGridTag)
+                        .layout { measurable, _ ->
+                            val crossAxis = if (expanded) {
+                                itemSizePx * 3
+                            } else {
+                                itemSizePx
+                            }
+                            val mainAxis = itemSizePx * 3 + 1
+                            val placeable = measurable.measure(
+                                Constraints.fixed(
+                                    width = if (vertical) crossAxis else mainAxis,
+                                    height = if (vertical) mainAxis else crossAxis
+                                )
+                            )
+                            layout(placeable.width, placeable.height) {
+                                placeable.place(IntOffset.Zero)
+                            }
+                        },
+                    state
+                ) {
+                    items(
+                        count = 100,
+                        span = {
+                            if (it == 0 || it == 5) GridItemSpan(maxLineSpan) else GridItemSpan(1)
+                        }
+                    ) { index ->
+                        Box(Modifier.size(itemSizeDp).testTag("$index").debugBorder())
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+
+        expanded = false
+        rule.waitForIdle()
+
+        expanded = true
+        rule.waitForIdle()
+    }
 }
 
 internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
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/BaseLazyStaggeredGridWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
index 3df360d..53ecd58 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
@@ -20,14 +20,12 @@
 import androidx.compose.foundation.AutoTestFrameClock
 import androidx.compose.foundation.BaseLazyLayoutTestWithOrientation
 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.layout.Arrangement
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
@@ -52,8 +50,6 @@
         }
     }
 
-    internal fun Modifier.debugBorder(color: Color = Color.Black) = border(1.dp, color)
-
     @Composable
     internal fun LazyStaggeredGrid(
         lanes: Int,
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/DrawPhaseAttributesToggleTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DrawPhaseAttributesToggleTest.kt
index 042769b..4964d92 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DrawPhaseAttributesToggleTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DrawPhaseAttributesToggleTest.kt
@@ -177,7 +177,7 @@
 
         rule.setContent {
             BasicText(
-                "ABC",
+                "TextPainter",
                 style = style,
                 modifier = Modifier.testTag(textTag)
             )
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/TextFieldFocusTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
index 2ad596e..02827ab 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
@@ -68,6 +68,7 @@
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -380,6 +381,7 @@
         rule.onNodeWithTag("test-button-bottom").assertIsFocused()
     }
 
+    @Ignore // b/264919150
     @Test
     fun basicTextField_checkKeyboardShown_onDPadCenter() {
         setupAndEnableBasicTextField()
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 8656483..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
@@ -43,6 +43,7 @@
 import androidx.compose.ui.text.input.VisualTransformation
 import com.google.common.truth.Truth.assertThat
 import kotlin.math.roundToInt
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
@@ -205,6 +206,7 @@
         assertThat(cursorPositions).isEqualTo(expectedCursorPositions)
     }
 
+    @Ignore // b/265023621
     @Test
     fun textField_extendsSelection_toRight() {
         textField_extendsSelection(
@@ -215,6 +217,7 @@
         )
     }
 
+    @Ignore // b/265023621
     @Test
     fun textField_extendsSelection_withPasswordVisualTransformation_toRight() {
         textField_extendsSelection(
@@ -225,6 +228,7 @@
         )
     }
 
+    @Ignore // b/265023621
     @Test
     fun textField_extendsSelection_withReducedVisualTransformation_toRight() {
         textField_extendsSelection(
@@ -238,6 +242,7 @@
         )
     }
 
+    @Ignore // b/265023420
     @Test
     fun textField_extendsSelection_toLeft() {
         textField_extendsSelection(
@@ -248,6 +253,7 @@
         )
     }
 
+    @Ignore // b/265023621
     @Test
     fun textField_extendsSelection_withPasswordVisualTransformation_toLeft() {
         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 bc1c269..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
@@ -44,6 +44,7 @@
 import kotlin.math.roundToInt
 import kotlin.test.assertFailsWith
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -183,7 +184,7 @@
         assertValidMessage(error, sourceIndex = 0, toTransformed = false)
     }
 
-    @FlakyTest(bugId = 241572024)
+    @Ignore // b/241572024
     @Test
     fun selectionEnd_throws_onStart_whenInvalidOriginalToTransformed() {
         rule.runOnIdle {
@@ -212,6 +213,7 @@
         assertValidMessage(error, sourceIndex = 0, toTransformed = false)
     }
 
+    @Ignore // b/241572024
     @Test
     fun selectionStart_throws_onDrag_whenInvalidOriginalToTransformed() {
         rule.onNodeWithTag(testTag).performTouchInput { longClick() }
@@ -227,6 +229,7 @@
         assertValidMessage(error, sourceIndex = 0, toTransformed = true)
     }
 
+    @Ignore // b/241572024
     @Test
     fun selectionStart_throws_onDrag_whenInvalidTransformedToOriginal() {
         rule.onNodeWithTag(testTag).performTouchInput { longClick() }
@@ -242,6 +245,7 @@
         assertValidMessage(error, sourceIndex = 0, toTransformed = false)
     }
 
+    @Ignore // b/265019668
     @Test
     fun selectionEnd_throws_onDrag_whenInvalidOriginalToTransformed() {
         rule.onNodeWithTag(testTag).performTouchInput { longClick() }
@@ -257,6 +261,7 @@
         assertValidMessage(error, sourceIndex = text.length, toTransformed = true)
     }
 
+    @Ignore // b/265019668
     @Test
     fun selectionEnd_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/Clickable.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Clickable.android.kt
index 79247de..6b5cef6 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Clickable.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Clickable.android.kt
@@ -19,13 +19,37 @@
 import android.view.KeyEvent.KEYCODE_DPAD_CENTER
 import android.view.KeyEvent.KEYCODE_ENTER
 import android.view.KeyEvent.KEYCODE_NUMPAD_ENTER
+import android.view.View
 import android.view.ViewConfiguration
+import android.view.ViewGroup
+import androidx.compose.runtime.Composable
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
 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.type
+import androidx.compose.ui.platform.LocalView
+
+@Composable
+internal actual fun isComposeRootInScrollableContainer(): () -> Boolean {
+    val view = LocalView.current
+    return {
+        view.isInScrollableViewGroup()
+    }
+}
+
+// Copied from View#isInScrollingContainer() which is @hide
+private fun View.isInScrollableViewGroup(): Boolean {
+    var p = parent
+    while (p != null && p is ViewGroup) {
+        if (p.shouldDelayChildPressedState()) {
+            return true
+        }
+        p = p.parent
+    }
+    return false
+}
 
 internal actual val TapIndicationDelay: Long = ViewConfiguration.getTapTimeout().toLong()
 
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/Clickable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
index 64643f1..b249514 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation
 
+import androidx.compose.foundation.gestures.ModifierLocalScrollableContainer
 import androidx.compose.foundation.gestures.PressGestureScope
 import androidx.compose.foundation.gestures.detectTapAndPress
 import androidx.compose.foundation.gestures.detectTapGestures
@@ -37,8 +38,8 @@
 import androidx.compose.ui.input.key.key
 import androidx.compose.ui.input.key.onKeyEvent
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.canScroll
-import androidx.compose.ui.input.consumeScrollContainerInfo
+import androidx.compose.ui.modifier.ModifierLocalConsumer
+import androidx.compose.ui.modifier.ModifierLocalReadScope
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.disabled
@@ -143,8 +144,11 @@
                 currentKeyPressInteractions
             )
         }
-
-        val delayPressInteraction = remember { mutableStateOf({ true }) }
+        val isRootInScrollableContainer = isComposeRootInScrollableContainer()
+        val isClickableInScrollableContainer = remember { mutableStateOf(true) }
+        val delayPressInteraction = rememberUpdatedState {
+            isClickableInScrollableContainer.value || isRootInScrollableContainer()
+        }
         val centreOffset = remember { mutableStateOf(Offset.Zero) }
 
         val gesture = Modifier.pointerInput(interactionSource, enabled) {
@@ -164,9 +168,18 @@
             )
         }
         Modifier
-            .consumeScrollContainerInfo { scrollContainerInfo ->
-                delayPressInteraction.value = { scrollContainerInfo?.canScroll() == true }
-            }
+            .then(
+                remember {
+                    object : ModifierLocalConsumer {
+                        override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
+                            with(scope) {
+                                isClickableInScrollableContainer.value =
+                                    ModifierLocalScrollableContainer.current
+                            }
+                        }
+                    }
+                }
+            )
             .genericClickableWithoutGesture(
                 gestureModifiers = gesture,
                 interactionSource = interactionSource,
@@ -317,9 +330,13 @@
                 currentKeyPressInteractions
             )
         }
-
-        val delayPressInteraction = remember { mutableStateOf({ true }) }
+        val isRootInScrollableContainer = isComposeRootInScrollableContainer()
+        val isClickableInScrollableContainer = remember { mutableStateOf(true) }
+        val delayPressInteraction = rememberUpdatedState {
+            isClickableInScrollableContainer.value || isRootInScrollableContainer()
+        }
         val centreOffset = remember { mutableStateOf(Offset.Zero) }
+
         val gesture =
             Modifier.pointerInput(interactionSource, hasLongClick, hasDoubleClick, enabled) {
                 centreOffset.value = size.center.toOffset()
@@ -348,6 +365,18 @@
                 )
             }
         Modifier
+            .then(
+                remember {
+                    object : ModifierLocalConsumer {
+                        override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
+                            with(scope) {
+                                isClickableInScrollableContainer.value =
+                                    ModifierLocalScrollableContainer.current
+                            }
+                        }
+                    }
+                }
+            )
             .genericClickableWithoutGesture(
                 gestureModifiers = gesture,
                 interactionSource = interactionSource,
@@ -362,9 +391,6 @@
                 onLongClick = onLongClick,
                 onClick = onClick
             )
-            .consumeScrollContainerInfo { scrollContainerInfo ->
-                delayPressInteraction.value = { scrollContainerInfo?.canScroll() == true }
-            }
     },
     inspectorInfo = debugInspectorInfo {
         name = "combinedClickable"
@@ -449,6 +475,20 @@
 internal expect val TapIndicationDelay: Long
 
 /**
+ * Returns a lambda that calculates whether the root Compose layout node is hosted in a scrollable
+ * container outside of Compose. On Android this will be whether the root View is in a scrollable
+ * ViewGroup, as even if nothing in the Compose part of the hierarchy is scrollable, if the View
+ * itself is in a scrollable container, we still want to delay presses in case presses in Compose
+ * convert to a scroll outside of Compose.
+ *
+ * Combine this with [ModifierLocalScrollableContainer], which returns whether a [Modifier] is
+ * within a scrollable Compose layout, to calculate whether this modifier is within some form of
+ * scrollable container, and hence should delay presses.
+ */
+@Composable
+internal expect fun isComposeRootInScrollableContainer(): () -> Boolean
+
+/**
  * Whether the specified [KeyEvent] should trigger a press for a clickable component.
  */
 internal expect val KeyEvent.isPress: Boolean
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/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index 0be2c7b..020772e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -24,7 +24,6 @@
 import androidx.compose.foundation.gestures.DragEvent.DragStopped
 import androidx.compose.foundation.interaction.DragInteraction
 import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.internal.JvmDefaultWithCompatibility
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
@@ -56,6 +55,7 @@
 import kotlinx.coroutines.channels.SendChannel
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.isActive
+import androidx.compose.foundation.internal.JvmDefaultWithCompatibility
 
 /**
  * State of [draggable]. Allows for a granular control of how deltas are consumed by the user as
@@ -258,7 +258,6 @@
             }
         }
     }
-
     Modifier.pointerInput(orientation, enabled, reverseDirection) {
         if (!enabled) return@pointerInput
         coroutineScope {
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 3876703..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
@@ -37,7 +37,6 @@
 import androidx.compose.ui.MotionDurationScale
 import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.ScrollContainerInfo
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
@@ -49,7 +48,8 @@
 import androidx.compose.ui.input.pointer.PointerEventType
 import androidx.compose.ui.input.pointer.PointerType
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.provideScrollContainerInfo
+import androidx.compose.ui.modifier.ModifierLocalProvider
+import androidx.compose.ui.modifier.modifierLocalOf
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.Density
@@ -172,6 +172,7 @@
                 overscrollEffect,
                 enabled
             )
+            .then(if (enabled) ModifierLocalScrollableContainerProvider else Modifier)
     }
 )
 
@@ -265,14 +266,6 @@
     val draggableState = remember { ScrollDraggableState(scrollLogic) }
     val scrollConfig = platformScrollConfig()
 
-    val scrollContainerInfo = remember(orientation, enabled) {
-        object : ScrollContainerInfo {
-            override fun canScrollHorizontally() = enabled && orientation == Horizontal
-
-            override fun canScrollVertically() = enabled && orientation == Orientation.Vertical
-        }
-    }
-
     return draggable(
         draggableState,
         orientation = orientation,
@@ -289,7 +282,6 @@
     )
         .mouseWheelScroll(scrollLogic, scrollConfig)
         .nestedScroll(nestedScrollConnection, nestedScrollDispatcher.value)
-        .provideScrollContainerInfo(scrollContainerInfo)
 }
 
 private fun Modifier.mouseWheelScroll(
@@ -363,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
@@ -432,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
@@ -591,9 +559,21 @@
     }
 }
 
+// TODO: b/203141462 - make this public and move it to ui
+/**
+ * Whether this modifier is inside a scrollable container, provided by [Modifier.scrollable].
+ * Defaults to false.
+ */
+internal val ModifierLocalScrollableContainer = modifierLocalOf { false }
+
+private object ModifierLocalScrollableContainerProvider : ModifierLocalProvider<Boolean> {
+    override val key = ModifierLocalScrollableContainer
+    override val value = true
+}
+
 private const val DefaultScrollMotionDurationScaleFactor = 1f
 
 internal val DefaultScrollMotionDurationScale = object : MotionDurationScale {
     override val scaleFactor: Float
         get() = DefaultScrollMotionDurationScaleFactor
-}
+}
\ No newline at end of file
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..f2b57ef 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.LazyPinnableContainerProvider
 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) {
+            LazyPinnableContainerProvider(state.pinnedItems, index) {
                 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..638ed8a 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
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.fastFilter
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.lazy.layout.LazyPinnedItem
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.Constraints
@@ -56,7 +57,7 @@
     placementAnimator: LazyListItemPlacementAnimator,
     beyondBoundsInfo: LazyListBeyondBoundsInfo,
     beyondBoundsItemCount: Int,
-    pinnedItems: List<LazyListPinnedItem>,
+    pinnedItems: List<LazyPinnedItem>,
     layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
 ): LazyListMeasureResult {
     require(beforeContentPadding >= 0)
@@ -75,7 +76,8 @@
             totalItemsCount = 0,
             reverseLayout = reverseLayout,
             orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
-            afterContentPadding = afterContentPadding
+            afterContentPadding = afterContentPadding,
+            mainAxisItemSpacing = spaceBetweenItems
         )
     } else {
         var currentFirstItemIndex = firstVisibleItemIndex
@@ -326,7 +328,8 @@
             totalItemsCount = itemsCount,
             reverseLayout = reverseLayout,
             orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal,
-            afterContentPadding = afterContentPadding
+            afterContentPadding = afterContentPadding,
+            mainAxisItemSpacing = spaceBetweenItems
         )
     }
 }
@@ -337,7 +340,7 @@
     itemProvider: LazyMeasuredItemProvider,
     itemsCount: Int,
     beyondBoundsItemCount: Int,
-    pinnedItems: List<LazyListPinnedItem>
+    pinnedItems: List<LazyPinnedItem>
 ): List<LazyMeasuredItem> {
     fun LazyListBeyondBoundsInfo.endIndex() = min(end, itemsCount - 1)
 
@@ -377,7 +380,7 @@
     itemProvider: LazyMeasuredItemProvider,
     itemsCount: Int,
     beyondBoundsItemCount: Int,
-    pinnedItems: List<LazyListPinnedItem>
+    pinnedItems: List<LazyPinnedItem>
 ): List<LazyMeasuredItem> {
     fun LazyListBeyondBoundsInfo.startIndex() = min(start, itemsCount - 1)
 
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..2306d58 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.LazyPinnedItemContainer
 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
@@ -224,7 +224,7 @@
     /**
      * List of extra items to compose during the measure pass.
      */
-    internal val pinnedItems = mutableStateListOf<LazyListPinnedItem>()
+    internal val pinnedItems = LazyPinnedItemContainer()
 
     /**
      * 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..562daad 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.LazyPinnableContainerProvider
 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)
+        LazyPinnableContainerProvider(state.pinnedItems, index) {
+            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..9fc620e 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
@@ -16,9 +16,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.LazyPinnedItem
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.Constraints
@@ -56,6 +57,7 @@
     density: Density,
     placementAnimator: LazyGridItemPlacementAnimator,
     spanLayoutProvider: LazyGridSpanLayoutProvider,
+    pinnedItems: List<LazyPinnedItem>,
     layout: (Int, Int, Placeable.PlacementScope.() -> Unit) -> MeasureResult
 ): LazyGridMeasureResult {
     require(beforeContentPadding >= 0)
@@ -74,7 +76,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 +97,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 +149,6 @@
         ) {
             val measuredLine = measuredLineProvider.getAndMeasure(index)
             if (measuredLine.isEmpty()) {
-                --index
                 break
             }
 
@@ -202,6 +204,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 +248,8 @@
 
         val positionedItems = calculateItemsOffsets(
             lines = visibleLines,
+            itemsBefore = extraItemsBefore,
+            itemsAfter = extraItemsAfter,
             layoutWidth = layoutWidth,
             layoutHeight = layoutHeight,
             finalMainAxisOffset = currentMainAxisOffset,
@@ -254,28 +274,60 @@
         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
         )
     }
 }
 
+private inline fun calculateExtraItems(
+    pinnedItems: List<LazyPinnedItem>,
+    itemProvider: LazyMeasuredItemProvider,
+    itemConstraints: (ItemIndex) -> Constraints,
+    filter: (Int) -> Boolean
+): List<LazyGridMeasuredItem> {
+    var items: MutableList<LazyGridMeasuredItem>? = null
+
+    pinnedItems.fastForEach {
+        val index = ItemIndex(it.index)
+        if (filter(it.index)) {
+            val constraints = itemConstraints(index)
+            val item = itemProvider.getAndMeasure(index, constraints = constraints)
+            if (items == null) {
+                items = mutableListOf()
+            }
+            items?.add(item)
+        }
+    }
+
+    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 +348,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 +387,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 6c0f594..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
@@ -224,6 +224,7 @@
         buckets.add(Bucket(0))
         lastLineIndex = 0
         lastLineStartItemIndex = 0
+        lastLineStartKnownSpan = 0
         cachedBucketIndex = -1
         cachedBucket.clear()
     }
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..7a40f09 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.LazyPinnedItemContainer
 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)
 
     /**
+     * Pinned items are measured and placed even when they are beyond bounds of lazy layout.
+     */
+    internal val pinnedItems = LazyPinnedItemContainer()
+
+    /**
      * 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/LazyPinnableContainerProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyPinnableContainerProvider.kt
new file mode 100644
index 0000000..5c22dc1
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyPinnableContainerProvider.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.foundation.lazy.layout
+
+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
+
+internal interface LazyPinnedItem {
+    val index: Int
+}
+
+internal class LazyPinnedItemContainer(
+    private val pinnedItems: MutableList<LazyPinnedItem> = SnapshotStateList()
+) : List<LazyPinnedItem> by pinnedItems {
+    fun pin(item: LazyPinnedItem) {
+        pinnedItems.add(item)
+    }
+
+    fun unpin(item: LazyPinnedItem) {
+        pinnedItems.remove(item)
+    }
+}
+
+@Composable
+internal fun LazyPinnableContainerProvider(
+    owner: LazyPinnedItemContainer,
+    index: Int,
+    content: @Composable () -> Unit
+) {
+    val pinnableItem = remember(owner) { LazyPinnableItem(owner) }
+    pinnableItem.index = index
+    pinnableItem.parentPinnableContainer = LocalPinnableContainer.current
+    DisposableEffect(pinnableItem) { onDispose { pinnableItem.onDisposed() } }
+    CompositionLocalProvider(
+        LocalPinnableContainer provides pinnableItem, content = content
+    )
+}
+
+private class LazyPinnableItem(
+    private val owner: LazyPinnedItemContainer,
+) : PinnableContainer, PinnableContainer.PinnedHandle, LazyPinnedItem {
+    /**
+     * 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) {
+            owner.pin(this)
+            parentHandle = parentPinnableContainer?.pin()
+        }
+        pinsCount++
+        return this
+    }
+
+    override fun release() {
+        check(pinsCount > 0) { "Release should only be called once" }
+        pinsCount--
+        if (pinsCount == 0) {
+            owner.unpin(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..7ce5cff 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.LazyPinnableContainerProvider
 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 ->
+                    LazyPinnableContainerProvider(state.pinnedItems, index) {
+                        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..d415800 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,9 @@
 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.layout.LazyPinnedItem
+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 +32,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 +88,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 +138,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 +154,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 +200,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 +216,7 @@
                 viewportEndOffset = mainAxisAvailableSize + afterContentPadding,
                 beforeContentPadding = beforeContentPadding,
                 afterContentPadding = afterContentPadding,
+                mainAxisItemSpacing = mainAxisSpacing
             )
         }
 
@@ -202,8 +234,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 +247,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 +257,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 +351,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 +366,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 +378,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 +453,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 +544,7 @@
 
                 if (previousIndex < 0) {
                     if (misalignedStart(laneIndex) && canRestartMeasure) {
-                        spans.reset()
+                        laneInfo.reset()
                         return measure(
                             initialScrollDelta = scrollDelta,
                             initialItemIndices = IntArray(firstItemIndices.size) { -1 },
@@ -446,15 +557,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 +584,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 +612,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 +630,72 @@
             }
         }
 
+        debugLog {
+            "| final first indices: ${firstItemIndices.toList()}, " +
+                "offsets: ${firstItemOffsets.toList()}"
+        }
+
         // end measure
 
+        var extraItemOffset = itemScrollOffsets[0]
+        val extraItemsBefore = calculateExtraItems(
+            state.pinnedItems,
+            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(
+            state.pinnedItems,
+            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 +706,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 +718,129 @@
             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
+}
+
+private inline fun LazyStaggeredGridMeasureContext.calculateExtraItems(
+    pinnedItems: List<LazyPinnedItem>,
+    position: (LazyStaggeredGridMeasuredItem) -> LazyStaggeredGridPositionedItem,
+    filter: (itemIndex: Int) -> Boolean
+): List<LazyStaggeredGridPositionedItem> {
+    var result: MutableList<LazyStaggeredGridPositionedItem>? = null
+
+    pinnedItems.fastForEach {
+        if (filter(it.index)) {
+            val spanRange = itemProvider.getSpanRange(it.index, 0)
+            if (result == null) {
+                result = mutableListOf()
+            }
+            val measuredItem = measuredItemProvider.getAndMeasure(it.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 +889,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 +910,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 +924,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 +936,7 @@
     fun createItem(
         index: Int,
         lane: Int,
+        span: Int,
         key: Any,
         placeables: List<Placeable>
     ): LazyStaggeredGridMeasuredItem
@@ -689,8 +948,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..82c3e4b 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.LazyPinnedItemContainer
 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()
 
     /**
+     * List of extra items to compose during the measure pass.
+     */
+    internal val pinnedItems = LazyPinnedItemContainer()
+
+    /**
      * 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/relocation/BringIntoViewRequester.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
index d178c065..0636f8c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
@@ -29,16 +29,16 @@
 
 /**
  * Can be used to send [bringIntoView] requests. Pass it as a parameter to
- * [Modifier.bringIntoView()][bringIntoView].
+ * [Modifier.bringIntoViewRequester()][bringIntoViewRequester].
  *
- * For instance, you can call [BringIntoViewRequester.bringIntoView][bringIntoView] to
- * make all the scrollable parents scroll so that the specified item is brought into parent
- * bounds. This sample demonstrates this use case:
+ * For instance, you can call [bringIntoView()][bringIntoView] to make all the
+ * scrollable parents scroll so that the specified item is brought into the
+ * parent bounds.
  *
  * Here is a sample where a composable is brought into view:
  * @sample androidx.compose.foundation.samples.BringIntoViewSample
  *
- * Here is a sample where a part of a composable is brought into view:
+ * Here is a sample where part of a composable is brought into view:
  * @sample androidx.compose.foundation.samples.BringPartOfComposableIntoViewSample
  */
 @ExperimentalFoundationApi
@@ -82,15 +82,20 @@
 }
 
 /**
- * This is a modifier that can be used to send bringIntoView requests.
+ * Modifier that can be used to send
+ * [bringIntoView][BringIntoViewRequester.bringIntoView] requests.
  *
- * Here is an example where the a [bringIntoViewRequester] can be used to bring an item into parent
- * bounds. It demonstrates how a composable can ask its parents to scroll so that the component
- * using this modifier is brought into the bounds of all its parents.
+ * The following example uses a `bringIntoViewRequester` to bring an item into
+ * the parent bounds. The example demonstrates how a composable can ask its
+ * parents to scroll so that the component using this modifier is brought into
+ * the bounds of all its parents.
+ *
  * @sample androidx.compose.foundation.samples.BringIntoViewSample
  *
- * @param bringIntoViewRequester an instance of [BringIntoViewRequester]. This hoisted object can be
- * used to send bringIntoView requests to parents of the current composable.
+ * @param bringIntoViewRequester An instance of [BringIntoViewRequester]. This
+ *     hoisted object can be used to send
+ *     [bringIntoView][BringIntoViewRequester.bringIntoView] requests to parents
+ *     of the current composable.
  */
 @ExperimentalFoundationApi
 fun Modifier.bringIntoViewRequester(
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/SelectionAdjustment.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustment.kt
index 9a3a76d..3de82bb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustment.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionAdjustment.kt
@@ -180,7 +180,7 @@
          * selection.
          *  b.if the previous start/end offset is a word boundary, use word based selection.
          *
-         *  Notice that this selection adjustment assumes that when isStartHandle is ture, only
+         *  Notice that this selection adjustment assumes that when isStartHandle is true, only
          *  start handle is moving(or unchanged), and vice versa.
          */
         val CharacterWithWordAccelerate = object : SelectionAdjustment {
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/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMode.kt
index 5c1ef9e..23c749d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMode.kt
@@ -99,7 +99,7 @@
             return true
         }
         // Compare the location of start and end to the bound. If both are on the same side, return
-        // false, otherwise return ture.
+        // false, otherwise return true.
         val compareStart = compare(start, bounds)
         val compareEnd = compare(end, bounds)
         return (compareStart > 0) xor (compareEnd > 0)
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
index 48eb943..6d663a8 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.gestures.awaitEachGesture
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
+import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -51,6 +52,9 @@
 import androidx.compose.ui.util.fastAll
 import java.awt.event.KeyEvent.VK_ENTER
 
+@Composable
+internal actual fun isComposeRootInScrollableContainer(): () -> Boolean = { false }
+
 // TODO: b/168524931 - should this depend on the input device?
 internal actual val TapIndicationDelay: Long = 0L
 
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..e54f249 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -483,8 +483,10 @@
 
   public final class InteractiveComponentSizeKt {
     method @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalMinimumInteractiveComponentEnforcement();
+    method @Deprecated @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> getLocalMinimumTouchTargetEnforcement();
     method public static androidx.compose.ui.Modifier minimumInteractiveComponentSize(androidx.compose.ui.Modifier);
     property @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumInteractiveComponentEnforcement;
+    property @Deprecated @androidx.compose.material.ExperimentalMaterialApi public static final androidx.compose.runtime.ProvidableCompositionLocal<java.lang.Boolean> LocalMinimumTouchTargetEnforcement;
   }
 
   public final class ListItemKt {
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index ebd1cd5..53e8375 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -48,7 +48,7 @@
         // TODO: remove next 3 dependencies when b/202810604 is fixed
         implementation("androidx.savedstate:savedstate:1.1.0")
         implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
-        implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
+        implementation(project(":lifecycle:lifecycle-viewmodel"))
 
         testImplementation(libs.testRules)
         testImplementation(libs.testRunner)
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
index 59a1fb9..391ce209 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
@@ -26,8 +26,6 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.canScroll
-import androidx.compose.ui.input.consumeScrollContainerInfo
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
@@ -1205,76 +1203,4 @@
         topNode = rule.onNodeWithTag(topTag).fetchSemanticsNode()
         assertEquals(2, topNode.children.size)
     }
-
-    @Test
-    fun modalDrawer_providesScrollableContainerInfo_enabled() {
-        var actualValue = { false }
-        rule.setMaterialContent {
-            ModalDrawer(
-                drawerContent = {},
-                content = {
-                    Box(Modifier.consumeScrollContainerInfo {
-                        actualValue = { it!!.canScroll() }
-                    })
-                }
-            )
-        }
-
-        assertThat(actualValue()).isTrue()
-    }
-
-    @Test
-    fun modalDrawer_providesScrollableContainerInfo_disabled() {
-        var actualValue = { false }
-        rule.setMaterialContent {
-            ModalDrawer(
-                drawerContent = {},
-                gesturesEnabled = false,
-                content = {
-                    Box(Modifier.consumeScrollContainerInfo {
-                        actualValue = { it!!.canScroll() }
-                    })
-                }
-            )
-        }
-
-        assertThat(actualValue()).isFalse()
-    }
-
-    @OptIn(ExperimentalMaterialApi::class)
-    @Test
-    fun bottomDrawer_providesScrollableContainerInfo_enabled() {
-        var actualValue = { false }
-        rule.setMaterialContent {
-            BottomDrawer(
-                drawerContent = {},
-                content = {
-                    Box(Modifier.consumeScrollContainerInfo {
-                        actualValue = { it!!.canScroll() }
-                    })
-                }
-            )
-        }
-
-        assertThat(actualValue()).isTrue()
-    }
-
-    @OptIn(ExperimentalMaterialApi::class)
-    @Test
-    fun bottomDrawer_providesScrollableContainerInfo_disabled() {
-        var actualValue = { false }
-        rule.setMaterialContent {
-            BottomDrawer(
-                drawerContent = {},
-                gesturesEnabled = false,
-                content = {
-                    Box(Modifier.consumeScrollContainerInfo {
-                        actualValue = { it!!.canScroll() }
-                    })
-                }
-            )
-        }
-
-        assertThat(actualValue()).isFalse()
-    }
 }
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 f697131..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
@@ -36,8 +36,6 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.canScroll
-import androidx.compose.ui.input.consumeScrollContainerInfo
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.platform.testTag
@@ -269,7 +267,7 @@
                 sheetContent = {
                     Box(
                         Modifier
-                            .fillMaxSize()
+                            .fillMaxSize(0.6f)
                             .testTag(sheetTag)
                     )
                 }
@@ -834,56 +832,6 @@
     }
 
     @Test
-    fun modalBottomSheet_providesScrollableContainerInfo_hidden() {
-        var actualValue = { false }
-        rule.setMaterialContent {
-            ModalBottomSheetLayout(
-                sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden),
-                content = { Box(
-                    Modifier
-                        .fillMaxSize()
-                        .testTag(contentTag)) },
-                sheetContent = {
-                    Box(
-                        Modifier
-                            .fillMaxSize()
-                            .consumeScrollContainerInfo {
-                                actualValue = { it!!.canScroll() }
-                            }
-                    )
-                }
-            )
-        }
-
-        assertThat(actualValue()).isFalse()
-    }
-
-    @Test
-    fun modalBottomSheet_providesScrollableContainerInfo_expanded() {
-        var actualValue = { false }
-        rule.setMaterialContent {
-            ModalBottomSheetLayout(
-                sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Expanded),
-                content = { Box(
-                    Modifier
-                        .fillMaxSize()
-                        .testTag(contentTag)) },
-                sheetContent = {
-                    Box(
-                        Modifier
-                            .fillMaxSize()
-                            .consumeScrollContainerInfo {
-                                actualValue = { it!!.canScroll() }
-                            }
-                    )
-                }
-            )
-        }
-
-        assertThat(actualValue()).isTrue()
-    }
-
-    @Test
     fun modalBottomSheet_nestedScroll_consumesWithinBounds_scrollsOutsideBounds() {
         lateinit var sheetState: ModalBottomSheetState
         lateinit var scrollState: ScrollState
@@ -1104,7 +1052,6 @@
                     Box(
                         Modifier
                             .testTag(sheetTag)
-                            .fillMaxWidth()
                             .fillMaxHeight(0.4f)
                     )
                 },
@@ -1133,6 +1080,7 @@
         val simulatedExpectedLeft = simulatedLeft + expectedSheetLeft
 
         rule.onNodeWithTag(sheetTag)
+            .onParent()
             .assertLeftPositionInRootIsEqualTo(
                 expectedLeft = simulatedExpectedLeft
             )
@@ -1155,7 +1103,6 @@
                     Box(
                         Modifier
                             .testTag(sheetTag)
-                            .fillMaxWidth()
                             .fillMaxHeight(0.4f)
                     )
                 },
@@ -1184,9 +1131,80 @@
         val simulatedExpectedLeft = simulatedLeft + expectedSheetLeft
 
         rule.onNodeWithTag(sheetTag)
+            .onParent()
             .assertLeftPositionInRootIsEqualTo(
                 expectedLeft = simulatedExpectedLeft
             )
             .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/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/Drawer.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
index 91f2417..8780e39 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
@@ -43,10 +43,8 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.isSpecified
-import androidx.compose.ui.input.ScrollContainerInfo
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.provideScrollContainerInfo
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
@@ -398,14 +396,6 @@
     content: @Composable () -> Unit
 ) {
     val scope = rememberCoroutineScope()
-
-    val containerInfo = remember(gesturesEnabled) {
-        object : ScrollContainerInfo {
-            override fun canScrollHorizontally() = gesturesEnabled
-
-            override fun canScrollVertically() = false
-        }
-    }
     BoxWithConstraints(modifier.fillMaxSize()) {
         val modalDrawerConstraints = constraints
         // TODO : think about Infinite max bounds case
@@ -434,7 +424,6 @@
                         DrawerValue.Open -> maxValue
                     }
                 }
-                .provideScrollContainerInfo(containerInfo)
         ) {
             Box {
                 content()
@@ -575,15 +564,6 @@
         } else {
             Modifier
         }
-
-        val containerInfo = remember(gesturesEnabled) {
-            object : ScrollContainerInfo {
-                override fun canScrollHorizontally() = gesturesEnabled
-
-                override fun canScrollVertically() = false
-            }
-        }
-
         val swipeable = Modifier
             .then(nestedScroll)
             .swipeable(
@@ -593,7 +573,6 @@
                 enabled = gesturesEnabled,
                 resistance = null
             )
-            .provideScrollContainerInfo(containerInfo)
 
         Box(swipeable) {
             content()
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 27d25255..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
@@ -27,6 +27,7 @@
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
 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.material.ModalBottomSheetState.Companion.Saver
@@ -46,12 +47,10 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.isSpecified
-import androidx.compose.ui.input.ScrollContainerInfo
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.provideScrollContainerInfo
 import androidx.compose.ui.semantics.collapse
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.dismiss
@@ -466,19 +465,11 @@
                 visible = sheetState.swipeableState.targetValue != Hidden
             )
         }
-
-        val containerInfo = remember(sheetState) {
-            object : ScrollContainerInfo {
-                override fun canScrollHorizontally() = false
-
-                override fun canScrollVertically() = sheetState.currentValue != Hidden
-            }
-        }
-
         Surface(
             Modifier
                 .align(Alignment.TopCenter) // We offset from the top so we'll center from there
                 .widthIn(max = MaxModalBottomSheetWidth)
+                .fillMaxWidth()
                 .nestedScroll(
                     remember(sheetState.swipeableState, orientation) {
                         ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
@@ -508,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) {
@@ -518,7 +509,6 @@
                         } else null
                     }
                 }
-                .provideScrollContainerInfo(containerInfo)
                 .semantics {
                     if (sheetState.isVisible) {
                         dismiss {
@@ -666,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/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 f0a3e12..5508abe 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -232,9 +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();
@@ -391,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 {
@@ -421,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();
@@ -500,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();
@@ -511,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;
@@ -594,16 +623,31 @@
   public final class ShapesKt {
   }
 
+  public final class SheetDefaultsKt {
+  }
+
   @androidx.compose.runtime.Immutable public final class SliderColors {
   }
 
-  public final class SliderDefaults {
+  @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 {
@@ -758,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 cccdf28..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,33 +296,48 @@
     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();
     method public kotlin.ranges.IntRange getYearRange();
+    property public final float TonalElevation;
     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 {
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void DatePickerDialog(kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, kotlin.jvm.functions.Function0<kotlin.Unit> confirmButton, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? dismissButton, optional androidx.compose.ui.graphics.Shape shape, optional float tonalElevation, optional androidx.compose.material3.DatePickerColors colors, optional androidx.compose.ui.window.DialogProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
   }
 
   @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;
   }
 
@@ -315,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();
@@ -352,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();
@@ -566,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();
@@ -587,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 {
@@ -617,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();
@@ -693,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();
@@ -709,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;
@@ -738,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;
@@ -821,28 +899,61 @@
   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 {
   }
 
-  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);
+  @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.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 int steps, optional kotlin.jvm.functions.Function0<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 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 {
-    ctor public SliderPositions(float initialPositionFraction, float[] initialTickFractions);
-    method public float getPositionFraction();
+  @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 float positionFraction;
+    property public final kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> activeRange;
     property public final float[] tickFractions;
   }
 
@@ -1053,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 f0a3e12..5508abe 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -232,9 +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();
@@ -391,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 {
@@ -421,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();
@@ -500,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();
@@ -511,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;
@@ -594,16 +623,31 @@
   public final class ShapesKt {
   }
 
+  public final class SheetDefaultsKt {
+  }
+
   @androidx.compose.runtime.Immutable public final class SliderColors {
   }
 
-  public final class SliderDefaults {
+  @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 {
@@ -758,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 e7d2e26..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,8 @@
 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
 import androidx.compose.material3.samples.DismissibleNavigationDrawerSample
@@ -97,6 +100,9 @@
 import androidx.compose.material3.samples.RadioButtonSample
 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
@@ -129,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
@@ -153,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 =
@@ -348,12 +366,26 @@
         DatePickerSample()
     },
     Example(
+        name = ::DatePickerDialogSample.name,
+        description = DatePickerExampleDescription,
+        sourceUrl = DatePickerExampleSourceUrl
+    ) {
+        DatePickerDialogSample()
+    },
+    Example(
         name = ::DatePickerWithDateValidatorSample.name,
         description = DatePickerExampleDescription,
         sourceUrl = DatePickerExampleSourceUrl
     ) {
         DatePickerWithDateValidatorSample()
     },
+    Example(
+        name = ::DateInputSample.name,
+        description = DatePickerExampleDescription,
+        sourceUrl = DatePickerExampleSourceUrl
+    ) {
+        DateInputSample()
+    },
 )
 
 private const val DialogExampleDescription = "Dialog examples"
@@ -722,6 +754,20 @@
         StepsSliderSample()
     },
     Example(
+        name = ::SliderWithCustomThumbSample.name,
+        description = SlidersExampleDescription,
+        sourceUrl = SlidersExampleSourceUrl
+    ) {
+        SliderWithCustomThumbSample()
+    },
+    Example(
+        name = ::SliderWithCustomTrackAndThumb.name,
+        description = SlidersExampleDescription,
+        sourceUrl = SlidersExampleSourceUrl
+    ) {
+        SliderWithCustomTrackAndThumb()
+    },
+    Example(
         name = ::RangeSliderSample.name,
         description = SlidersExampleDescription,
         sourceUrl = SlidersExampleSourceUrl
@@ -736,18 +782,11 @@
         StepRangeSliderSample()
     },
     Example(
-        name = ::SliderWithCustomThumbSample.name,
+        name = ::RangeSliderWithCustomComponents.name,
         description = SlidersExampleDescription,
         sourceUrl = SlidersExampleSourceUrl
     ) {
-        SliderWithCustomThumbSample()
-    },
-    Example(
-        name = ::SliderWithCustomTrackAndThumb.name,
-        description = SlidersExampleDescription,
-        sourceUrl = SlidersExampleSourceUrl
-    ) {
-        SliderWithCustomTrackAndThumb()
+        RangeSliderWithCustomComponents()
     }
 )
 
@@ -872,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(
@@ -981,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 70f82df..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
@@ -21,10 +21,19 @@
 import androidx.compose.foundation.layout.Column
 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
 import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
 import androidx.compose.material3.rememberDatePickerState
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
@@ -33,6 +42,7 @@
 import java.time.ZoneId
 import java.util.Calendar
 import java.util.TimeZone
+import kotlinx.coroutines.launch
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Preview
@@ -40,13 +50,65 @@
 @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"}")
     }
 }
 
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Sampled
+@Composable
+fun DatePickerDialogSample() {
+    // Decoupled snackbar host state from scaffold state for demo purposes.
+    val snackState = remember { SnackbarHostState() }
+    val snackScope = rememberCoroutineScope()
+    SnackbarHost(hostState = snackState, Modifier)
+    val openDialog = remember { mutableStateOf(true) }
+    // TODO demo how to read the selected date from the state.
+    if (openDialog.value) {
+        val datePickerState = rememberDatePickerState()
+        val confirmEnabled = derivedStateOf { datePickerState.selectedDateMillis != null }
+        DatePickerDialog(
+            onDismissRequest = {
+                // Dismiss the dialog when the user clicks outside the dialog or on the back
+                // button. If you want to disable that functionality, simply use an empty
+                // onDismissRequest.
+                openDialog.value = false
+            },
+            confirmButton = {
+                TextButton(
+                    onClick = {
+                        openDialog.value = false
+                        snackScope.launch {
+                            snackState.showSnackbar(
+                                "Selected date timestamp: ${datePickerState.selectedDateMillis}"
+                            )
+                        }
+                    },
+                    enabled = confirmEnabled.value
+                ) {
+                    Text("OK")
+                }
+            },
+            dismissButton = {
+                TextButton(
+                    onClick = {
+                        openDialog.value = false
+                    }
+                ) {
+                    Text("Cancel")
+                }
+            }
+        ) {
+            DatePicker(state = datePickerState)
+        }
+    }
+}
+
 @Suppress("ClassVerificationFailure")
 @OptIn(ExperimentalMaterial3Api::class)
 @Preview
@@ -56,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) {
@@ -74,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/SliderSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
index f1165b4..154bdd2 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/SliderSamples.kt
@@ -54,7 +54,6 @@
     }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
 @Preview
 @Sampled
 @Composable
@@ -80,49 +79,6 @@
 @Preview
 @Sampled
 @Composable
-fun RangeSliderSample() {
-    var sliderPosition by remember { mutableStateOf(0f..100f) }
-    Column {
-        Text(text = sliderPosition.toString())
-        RangeSlider(
-            modifier = Modifier.semantics { contentDescription = "Localized Description" },
-            value = sliderPosition,
-            onValueChange = { sliderPosition = it },
-            valueRange = 0f..100f,
-            onValueChangeFinished = {
-                // launch some business logic update with the state you hold
-                // viewModel.updateSelectedSliderValue(sliderPosition)
-            },
-        )
-    }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Preview
-@Sampled
-@Composable
-fun StepRangeSliderSample() {
-    var sliderPosition by remember { mutableStateOf(0f..100f) }
-    Column {
-        Text(text = sliderPosition.toString())
-        RangeSlider(
-            modifier = Modifier.semantics { contentDescription = "Localized Description" },
-            steps = 5,
-            value = sliderPosition,
-            onValueChange = { sliderPosition = it },
-            valueRange = 0f..100f,
-            onValueChangeFinished = {
-                // launch some business logic update with the state you hold
-                // viewModel.updateSelectedSliderValue(sliderPosition)
-            },
-        )
-    }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Preview
-@Sampled
-@Composable
 fun SliderWithCustomThumbSample() {
     var sliderPosition by remember { mutableStateOf(0f) }
     Column {
@@ -184,3 +140,94 @@
         )
     }
 }
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Sampled
+@Composable
+fun RangeSliderSample() {
+    var sliderPosition by remember { mutableStateOf(0f..100f) }
+    Column {
+        Text(text = sliderPosition.toString())
+        RangeSlider(
+            modifier = Modifier.semantics { contentDescription = "Localized Description" },
+            value = sliderPosition,
+            onValueChange = { sliderPosition = it },
+            valueRange = 0f..100f,
+            onValueChangeFinished = {
+                // launch some business logic update with the state you hold
+                // viewModel.updateSelectedSliderValue(sliderPosition)
+            },
+        )
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Sampled
+@Composable
+fun StepRangeSliderSample() {
+    var sliderPosition by remember { mutableStateOf(0f..100f) }
+    Column {
+        Text(text = sliderPosition.toString())
+        RangeSlider(
+            modifier = Modifier.semantics { contentDescription = "Localized Description" },
+            steps = 5,
+            value = sliderPosition,
+            onValueChange = { sliderPosition = it },
+            valueRange = 0f..100f,
+            onValueChangeFinished = {
+                // launch some business logic update with the state you hold
+                // viewModel.updateSelectedSliderValue(sliderPosition)
+            },
+        )
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Preview
+@Sampled
+@Composable
+fun RangeSliderWithCustomComponents() {
+    var sliderPosition by remember { mutableStateOf(0f..100f) }
+    val startInteractionSource = remember { MutableInteractionSource() }
+    val endInteractionSource = remember { MutableInteractionSource() }
+    val startThumbAndTrackColors = SliderDefaults.colors(
+        thumbColor = Color.Blue,
+        activeTrackColor = Color.Red
+    )
+    val endThumbColors = SliderDefaults.colors(thumbColor = Color.Green)
+    Column {
+        Text(text = sliderPosition.toString())
+        RangeSlider(
+            modifier = Modifier.semantics { contentDescription = "Localized Description" },
+            value = sliderPosition,
+            onValueChange = { sliderPosition = it },
+            valueRange = 0f..100f,
+            onValueChangeFinished = {
+                // launch some business logic update with the state you hold
+                // viewModel.updateSelectedSliderValue(sliderPosition)
+            },
+            startInteractionSource = startInteractionSource,
+            endInteractionSource = endInteractionSource,
+            startThumb = {
+                SliderDefaults.Thumb(
+                    interactionSource = startInteractionSource,
+                    colors = startThumbAndTrackColors
+                )
+            },
+            endThumb = {
+                SliderDefaults.Thumb(
+                    interactionSource = endInteractionSource,
+                    colors = endThumbColors
+                )
+            },
+            track = { sliderPositions ->
+                SliderDefaults.Track(
+                    colors = startThumbAndTrackColors,
+                    sliderPositions = sliderPositions
+                )
+            }
+        )
+    }
+}
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 5e28a15..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
@@ -24,6 +24,7 @@
 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.compose.ui.test.onNodeWithText
@@ -60,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
@@ -76,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
                 )
             }
         }
@@ -92,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
                 )
             }
         }
@@ -108,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
                 )
             }
         }
@@ -119,6 +139,33 @@
         assertAgainstGolden("datePicker_yearPicker_${scheme.name}")
     }
 
+    @Test
+    fun datePicker_inDialog() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            val monthInUtcMillis = dayInUtcMilliseconds(year = 2021, month = 3, dayOfMonth = 1)
+            val selectedDayMillis = dayInUtcMilliseconds(year = 2021, month = 3, dayOfMonth = 6)
+            DatePickerDialog(
+                onDismissRequest = { },
+                confirmButton = { TextButton(onClick = {}) { Text("OK") } },
+                dismissButton = { TextButton(onClick = {}) { Text("Cancel") } }
+            ) {
+                DatePicker(
+                    state = rememberDatePickerState(
+                        initialDisplayedMonthMillis = monthInUtcMillis,
+                        initialSelectedDateMillis = selectedDayMillis
+                    ),
+                    showModeToggle = false
+                )
+            }
+        }
+        rule.onNode(isDialog())
+            .captureToImage()
+            .assertAgainstGolden(
+                rule = screenshotRule,
+                goldenIdentifier = "datePicker_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 =
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/DismissibleNavigationDrawerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
index 6deaf4d..2edb2e5 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
@@ -26,8 +26,6 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.canScroll
-import androidx.compose.ui.input.consumeScrollContainerInfo
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
@@ -560,44 +558,6 @@
                 .onParent()
                 .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Dismiss))
         }
-
-    @Test
-    fun dismissibleNavigationDrawer_providesScrollableContainerInfo_enabled() {
-        var actualValue = { false }
-        rule.setMaterialContent(lightColorScheme()) {
-
-            DismissibleNavigationDrawer(
-                gesturesEnabled = true,
-                drawerContent = {},
-                content = {
-                    Box(Modifier.consumeScrollContainerInfo {
-                        actualValue = { it!!.canScroll() }
-                    })
-                }
-            )
-        }
-
-        assertThat(actualValue()).isTrue()
-    }
-
-    @Test
-    fun dismissibleNavigationDrawer_providesScrollableContainerInfo_disabled() {
-        var actualValue = { false }
-        rule.setMaterialContent(lightColorScheme()) {
-
-            DismissibleNavigationDrawer(
-                gesturesEnabled = false,
-                drawerContent = {},
-                content = {
-                    Box(Modifier.consumeScrollContainerInfo {
-                        actualValue = { it!!.canScroll() }
-                    })
-                }
-            )
-        }
-
-        assertThat(actualValue()).isFalse()
-    }
 }
 
 private val DrawerTestTag = "drawer"
\ No newline at end of file
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/ModalNavigationDrawerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
index f18376cc..409120f 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
@@ -26,8 +26,6 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.canScroll
-import androidx.compose.ui.input.consumeScrollContainerInfo
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
@@ -658,45 +656,6 @@
         topNode = rule.onNodeWithTag(topTag).fetchSemanticsNode()
         assertEquals(2, topNode.children.size)
     }
-
-    @Test
-    fun navigationDrawer_providesScrollableContainerInfo_enabled() {
-        var actualValue = { false }
-        rule.setMaterialContent(lightColorScheme()) {
-            ModalNavigationDrawer(
-                drawerContent = { ModalDrawerSheet { } },
-                content = {
-                    Box(
-                        Modifier.consumeScrollContainerInfo {
-                            actualValue = { it!!.canScroll() }
-                        }
-                    )
-                }
-            )
-        }
-
-        assertThat(actualValue()).isTrue()
-    }
-
-    @Test
-    fun navigationDrawer_providesScrollableContainerInfo_disabled() {
-        var actualValue = { false }
-        rule.setMaterialContent(lightColorScheme()) {
-            ModalNavigationDrawer(
-                gesturesEnabled = false,
-                drawerContent = { ModalDrawerSheet { } },
-                content = {
-                    Box(
-                        Modifier.consumeScrollContainerInfo {
-                            actualValue = { it!!.canScroll() }
-                        }
-                    )
-                }
-            )
-        }
-
-        assertThat(actualValue()).isFalse()
-    }
 }
 
 private val DrawerTestTag = "drawer"
\ No newline at end of file
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt
index e2f2a1c..24c2ae7 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SliderTest.kt
@@ -1176,6 +1176,76 @@
         rule.onAllNodes(isFocusable(), true)[1]
             .assertRangeInfoEquals(ProgressBarRangeInfo(15f, 10f..20f, 1))
     }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_thumb_recomposition() {
+        val state = mutableStateOf(0f..100f)
+        val startRecompositionCounter = SliderRecompositionCounter()
+        val endRecompositionCounter = SliderRecompositionCounter()
+
+        rule.setContent {
+            RangeSlider(
+                modifier = Modifier.testTag(tag),
+                value = state.value,
+                onValueChange = { state.value = it },
+                valueRange = 0f..100f,
+                startThumb = { sliderPositions ->
+                    startRecompositionCounter.OuterContent(sliderPositions)
+                },
+                endThumb = { sliderPositions ->
+                    endRecompositionCounter.OuterContent(sliderPositions)
+                }
+            )
+        }
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(100f, 0f))
+                moveBy(Offset(-100f, 0f))
+                moveBy(Offset(100f, 0f))
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(startRecompositionCounter.outerRecomposition).isEqualTo(1)
+            Truth.assertThat(startRecompositionCounter.innerRecomposition).isEqualTo(3)
+            Truth.assertThat(endRecompositionCounter.outerRecomposition).isEqualTo(1)
+            Truth.assertThat(endRecompositionCounter.innerRecomposition).isEqualTo(3)
+        }
+    }
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Test
+    fun rangeSlider_track_recomposition() {
+        val state = mutableStateOf(0f..100f)
+        val recompositionCounter = SliderRecompositionCounter()
+
+        rule.setContent {
+            RangeSlider(
+                modifier = Modifier.testTag(tag),
+                value = state.value,
+                onValueChange = { state.value = it },
+                valueRange = 0f..100f,
+                track = { sliderPositions ->
+                    recompositionCounter.OuterContent(sliderPositions)
+                }
+            )
+        }
+
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(100f, 0f))
+                moveBy(Offset(-100f, 0f))
+                moveBy(Offset(100f, 0f))
+            }
+
+        rule.runOnIdle {
+            Truth.assertThat(recompositionCounter.outerRecomposition).isEqualTo(1)
+            Truth.assertThat(recompositionCounter.innerRecomposition).isEqualTo(4)
+        }
+    }
 }
 
 @Stable
@@ -1197,6 +1267,6 @@
     @Composable
     private fun InnerContent(sliderPositions: SliderPositions) {
         SideEffect { ++innerRecomposition }
-        Text("InnerContent: ${sliderPositions.positionFraction}")
+        Text("InnerContent: ${sliderPositions.activeRange}")
     }
 }
\ No newline at end of file
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarTest.kt
index deb36d8..e0f7a77 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/SnackbarTest.kt
@@ -105,7 +105,7 @@
             }
         lateinit var dismissContentDescription: String
         rule.setMaterialContent(lightColorScheme()) {
-            dismissContentDescription = getString(string = Strings.Dismiss)
+            dismissContentDescription = getString(string = Strings.SnackbarDismiss)
             Box { Snackbar(snackbarData = snackbarData) }
         }
 
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..29d96f4
--- /dev/null
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TimePickerTest.kt
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.junit4.createComposeRule
+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.onNodeWithContentDescription(hourDescription)
+            .assertExists()
+            .onSiblings()
+            .filter(isFocusable())
+            .assertCountEquals(11)
+            .assertAll(
+                hasContentDescription(
+                    value = "o'clock",
+                    substring = true,
+                    ignoreCase = true
+                )
+            )
+    }
+
+    @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 32ea63a..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,6 +30,8 @@
 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
@@ -36,7 +39,9 @@
 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.launch
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -49,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)
@@ -61,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,9 +86,24 @@
     }
 
     @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",
@@ -93,31 +119,148 @@
     }
 
     @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()
@@ -131,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(
@@ -153,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
new file mode 100644
index 0000000..f77adef
--- /dev/null
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/DatePickerDialog.android.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.material3
+
+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.heightIn
+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
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.DialogProperties
+
+/**
+ * <a href="https://m3.material.io/components/date-pickers/overview" class="external" target="_blank">Material Design date picker dialog</a>.
+ *
+ * A dialog for displaying a [DatePicker]. Date pickers let people select a date.
+ *
+ * A sample for displaying a [DatePicker] in a dialog:
+ * @sample androidx.compose.material3.samples.DatePickerDialogSample
+ *
+ * @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside
+ * or pressing the back button. This is not called when the dismiss button is clicked.
+ * @param confirmButton button which is meant to confirm a proposed action, thus resolving what
+ * triggered the dialog. The dialog does not set up any events for this button, nor does it control
+ * its enablement, so those need to be set up by the caller.
+ * @param modifier the [Modifier] to be applied to this dialog's content.
+ * @param dismissButton button which is meant to dismiss the dialog. The dialog does not set up any
+ * events for this button so they need to be set up by the caller.
+ * @param shape defines the dialog's surface shape as well its shadow
+ * @param tonalElevation when [DatePickerColors.containerColor] is [ColorScheme.surface], a higher
+ * the elevation will result in a darker color in light theme and lighter color in dark theme
+ * @param colors [DatePickerColors] that will be used to resolve the colors used for this date
+ * picker in different states. See [DatePickerDefaults.colors].
+ * @param properties typically platform specific properties to further configure the dialog
+ * @param content the content of the dialog (i.e. a [DatePicker], for example)
+ */
+@ExperimentalMaterial3Api
+@Composable
+fun DatePickerDialog(
+    onDismissRequest: () -> Unit,
+    confirmButton: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    dismissButton: @Composable (() -> Unit)? = null,
+    shape: Shape = DatePickerDefaults.shape,
+    tonalElevation: Dp = DatePickerDefaults.TonalElevation,
+    colors: DatePickerColors = DatePickerDefaults.colors(),
+    properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false),
+    content: @Composable ColumnScope.() -> Unit
+) {
+    AlertDialog(
+        onDismissRequest = onDismissRequest,
+        modifier = modifier.wrapContentHeight(),
+        properties = properties
+    ) {
+        Surface(
+            modifier = Modifier
+                .requiredWidth(DatePickerModalTokens.ContainerWidth)
+                .heightIn(max = DatePickerModalTokens.ContainerHeight),
+            shape = shape,
+            color = colors.containerColor,
+            tonalElevation = tonalElevation,
+        ) {
+            Column(verticalArrangement = Arrangement.SpaceBetween) {
+                content()
+                // Buttons
+                Box(
+                    modifier = Modifier
+                        .align(Alignment.End)
+                        .padding(DialogButtonsPadding)
+                ) {
+                    CompositionLocalProvider(
+                        LocalContentColor provides DialogTokens.ActionLabelTextColor.toColor()
+                    ) {
+                        val textStyle =
+                            MaterialTheme.typography.fromToken(DialogTokens.ActionLabelTextFont)
+                        ProvideTextStyle(value = textStyle) {
+                            AlertDialogFlowRow(
+                                mainAxisSpacing = DialogButtonsMainAxisSpacing,
+                                crossAxisSpacing = DialogButtonsCrossAxisSpacing
+                            ) {
+                                dismissButton?.invoke()
+                                confirmButton()
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+private val DialogButtonsPadding = PaddingValues(bottom = 8.dp, end = 6.dp)
+private val DialogButtonsMainAxisSpacing = 8.dp
+private val DialogButtonsCrossAxisSpacing = 12.dp
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/SearchBar.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/SearchBar.kt
index 3671e1b..4047249 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/SearchBar.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/SearchBar.kt
@@ -401,7 +401,7 @@
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
 ) {
     val focusRequester = remember { FocusRequester() }
-    val searchSemantics = getString(Strings.Search)
+    val searchSemantics = getString(Strings.SearchBarSearch)
     val suggestionsAvailableSemantics = getString(Strings.SuggestionsAvailable)
     val textColor = LocalTextStyle.current.color.takeOrElse {
         colors.textColor(enabled).value
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 158c40b..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
@@ -36,28 +40,116 @@
         Strings.Dialog -> resources.getString(androidx.compose.material3.R.string.dialog)
         Strings.MenuExpanded -> resources.getString(androidx.compose.material3.R.string.expanded)
         Strings.MenuCollapsed -> resources.getString(androidx.compose.material3.R.string.collapsed)
-        Strings.Dismiss -> resources.getString(androidx.compose.material3.R.string.dismiss)
-        Strings.Search -> resources.getString(androidx.compose.material3.R.string.search)
+        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 8075df8..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,13 +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="dismiss" msgid="1461218791585306270">"Maak toe"</string>
-    <string name="search" msgid="8595876902241072592">"Soek"</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 ed2e3cf..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"መገናኛ"</string>
     <string name="expanded" msgid="5974471714631304645">"ተዘርግቷል"</string>
     <string name="collapsed" msgid="5389587048670450460">"ተሰብስቧል"</string>
-    <string name="dismiss" msgid="1461218791585306270">"አሰናብት"</string>
-    <string name="search" msgid="8595876902241072592">"ፍለጋ"</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 70f4f05..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"مربّع حوار"</string>
     <string name="expanded" msgid="5974471714631304645">"موسَّع"</string>
     <string name="collapsed" msgid="5389587048670450460">"مصغَّر"</string>
-    <string name="dismiss" msgid="1461218791585306270">"إغلاق"</string>
-    <string name="search" msgid="8595876902241072592">"بحث"</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 d7f3c72..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ডায়ল’গ"</string>
     <string name="expanded" msgid="5974471714631304645">"বিস্তাৰ কৰা আছে"</string>
     <string name="collapsed" msgid="5389587048670450460">"সংকোচন কৰা আছে"</string>
-    <string name="dismiss" msgid="1461218791585306270">"অগ্ৰাহ্য কৰক"</string>
-    <string name="search" msgid="8595876902241072592">"সন্ধান"</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 3b2fd43..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,13 +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="dismiss" msgid="1461218791585306270">"Qapadılsın"</string>
-    <string name="search" msgid="8595876902241072592">"Axtarış"</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 c8b3281..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,13 +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="dismiss" msgid="1461218791585306270">"Odbaci"</string>
-    <string name="search" msgid="8595876902241072592">"Pretraga"</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 5dcf47d..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Дыялогавае акно"</string>
     <string name="expanded" msgid="5974471714631304645">"Разгорнута"</string>
     <string name="collapsed" msgid="5389587048670450460">"Згорнута"</string>
-    <string name="dismiss" msgid="1461218791585306270">"Закрыць"</string>
-    <string name="search" msgid="8595876902241072592">"Пошук"</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 53c62d3..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Диалогов прозорец"</string>
     <string name="expanded" msgid="5974471714631304645">"Разгънато"</string>
     <string name="collapsed" msgid="5389587048670450460">"Свито"</string>
-    <string name="dismiss" msgid="1461218791585306270">"Отхвърляне"</string>
-    <string name="search" msgid="8595876902241072592">"Търсене"</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 bae5ac5..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ডায়ালগ বক্স"</string>
     <string name="expanded" msgid="5974471714631304645">"বড় করা হয়েছে"</string>
     <string name="collapsed" msgid="5389587048670450460">"আড়াল করা হয়েছে"</string>
-    <string name="dismiss" msgid="1461218791585306270">"বাতিল করুন"</string>
-    <string name="search" msgid="8595876902241072592">"সার্চ করুন"</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 592e753..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,13 +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="dismiss" msgid="1461218791585306270">"Odbacivanje"</string>
-    <string name="search" msgid="8595876902241072592">"Pretraživanje"</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 55f8816..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,13 +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="dismiss" msgid="1461218791585306270">"Ignora"</string>
-    <string name="search" msgid="8595876902241072592">"Cerca"</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 a33e105..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,13 +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="dismiss" msgid="1461218791585306270">"Zavřít"</string>
-    <string name="search" msgid="8595876902241072592">"Hledat"</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 4c9bcda..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,13 +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="dismiss" msgid="1461218791585306270">"Afvis"</string>
-    <string name="search" msgid="8595876902241072592">"Søg"</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 d86213e..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,13 +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="dismiss" msgid="1461218791585306270">"Ablehnen"</string>
-    <string name="search" msgid="8595876902241072592">"Suchen"</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 03afa6e..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Παράθυρο διαλόγου"</string>
     <string name="expanded" msgid="5974471714631304645">"Ανεπτυγμένο"</string>
     <string name="collapsed" msgid="5389587048670450460">"Συμπτυγμένο"</string>
-    <string name="dismiss" msgid="1461218791585306270">"Παράβλεψη"</string>
-    <string name="search" msgid="8595876902241072592">"Αναζήτηση"</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 d4ff0a6..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,13 +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="dismiss" msgid="1461218791585306270">"Dismiss"</string>
-    <string name="search" msgid="8595876902241072592">"Search"</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 adbcb28..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,13 +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="dismiss" msgid="1461218791585306270">"Dismiss"</string>
-    <string name="search" msgid="8595876902241072592">"Search"</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 d4ff0a6..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,13 +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="dismiss" msgid="1461218791585306270">"Dismiss"</string>
-    <string name="search" msgid="8595876902241072592">"Search"</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 d4ff0a6..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,13 +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="dismiss" msgid="1461218791585306270">"Dismiss"</string>
-    <string name="search" msgid="8595876902241072592">"Search"</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 5e13b3d..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,13 +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="dismiss" msgid="1461218791585306270">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‎‎‎‏‎‎‎‏‏‏‎‏‎‎‏‎‏‎‏‎‏‏‏‏‏‏‏‏‎‎‎‎‏‏‏‎‎‏‎‎‎‏‎‏‎‏‎‏‏‎‏‎‎‏‏‏‏‎‎Dismiss‎‏‎‎‏‎"</string>
-    <string name="search" msgid="8595876902241072592">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‏‎‏‎‏‎‏‏‎‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‎‎‏‎‎‎‎‎‏‏‏‎‏‎‏‏‏‎‏‎‎‎‎‎Search‎‏‎‎‏‎"</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 972a8f1..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,13 +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="dismiss" msgid="1461218791585306270">"Descartar"</string>
-    <string name="search" msgid="8595876902241072592">"Buscar"</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 3a866ce..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,13 +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="dismiss" msgid="1461218791585306270">"Cerrar"</string>
-    <string name="search" msgid="8595876902241072592">"Buscar"</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 9106900..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,13 +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="dismiss" msgid="1461218791585306270">"Loobu"</string>
-    <string name="search" msgid="8595876902241072592">"Otsing"</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 2c4a279..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,13 +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="dismiss" msgid="1461218791585306270">"Baztertu"</string>
-    <string name="search" msgid="8595876902241072592">"Bilatu"</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 a678350..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"کادر گفتگو"</string>
     <string name="expanded" msgid="5974471714631304645">"ازهم بازشده"</string>
     <string name="collapsed" msgid="5389587048670450460">"جمع‌شده"</string>
-    <string name="dismiss" msgid="1461218791585306270">"رد شدن"</string>
-    <string name="search" msgid="8595876902241072592">"جستجو"</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 490bd84..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,13 +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="dismiss" msgid="1461218791585306270">"Hylkää"</string>
-    <string name="search" msgid="8595876902241072592">"Haku"</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 e4d5587..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,13 +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="dismiss" msgid="1461218791585306270">"Fermer"</string>
-    <string name="search" msgid="8595876902241072592">"Recherche"</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 0cb1010..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,13 +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="dismiss" msgid="1461218791585306270">"Ignorer"</string>
-    <string name="search" msgid="8595876902241072592">"Rechercher"</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 ca68328..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,13 +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="dismiss" msgid="1461218791585306270">"Pechar"</string>
-    <string name="search" msgid="8595876902241072592">"Buscar"</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 55997a2..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"સંવાદ બૉક્સ"</string>
     <string name="expanded" msgid="5974471714631304645">"મોટી કરેલી"</string>
     <string name="collapsed" msgid="5389587048670450460">"નાની કરેલી"</string>
-    <string name="dismiss" msgid="1461218791585306270">"છોડી દો"</string>
-    <string name="search" msgid="8595876902241072592">"શોધો"</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 d8ded33..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"डायलॉग"</string>
     <string name="expanded" msgid="5974471714631304645">"बड़ा किया गया"</string>
     <string name="collapsed" msgid="5389587048670450460">"छोटा किया गया"</string>
-    <string name="dismiss" msgid="1461218791585306270">"खारिज करें"</string>
-    <string name="search" msgid="8595876902241072592">"खोजें"</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 f2bb389..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,13 +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="dismiss" msgid="1461218791585306270">"Odbaci"</string>
-    <string name="search" msgid="8595876902241072592">"Pretraživanje"</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 248168e..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,13 +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="dismiss" msgid="1461218791585306270">"Elvetés"</string>
-    <string name="search" msgid="8595876902241072592">"Keresés"</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 34a1312..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Երկխոսության պատուհան"</string>
     <string name="expanded" msgid="5974471714631304645">"Ծավալված է"</string>
     <string name="collapsed" msgid="5389587048670450460">"Ծալված է"</string>
-    <string name="dismiss" msgid="1461218791585306270">"Փակել"</string>
-    <string name="search" msgid="8595876902241072592">"Որոնել"</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 1edfa94..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,13 +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="dismiss" msgid="1461218791585306270">"Tutup"</string>
-    <string name="search" msgid="8595876902241072592">"Telusuri"</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 037f206..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,13 +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="dismiss" msgid="1461218791585306270">"Hunsa"</string>
-    <string name="search" msgid="8595876902241072592">"Leita"</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 690d277..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,13 +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="dismiss" msgid="1461218791585306270">"Chiudi"</string>
-    <string name="search" msgid="8595876902241072592">"Cerca"</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 8b653f1..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"תיבת דו-שיח"</string>
     <string name="expanded" msgid="5974471714631304645">"מורחב"</string>
     <string name="collapsed" msgid="5389587048670450460">"מכווץ"</string>
-    <string name="dismiss" msgid="1461218791585306270">"סגירה"</string>
-    <string name="search" msgid="8595876902241072592">"חיפוש"</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 272f0b3..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,19 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"ダイアログ"</string>
     <string name="expanded" msgid="5974471714631304645">"開いています"</string>
     <string name="collapsed" msgid="5389587048670450460">"閉じています"</string>
-    <string name="dismiss" msgid="1461218791585306270">"閉じる"</string>
-    <string name="search" msgid="8595876902241072592">"検索"</string>
+    <string name="snackbar_dismiss" msgid="3962933905051144957">"閉じる"</string>
+    <string name="search_bar_search" msgid="6420018528474762666">"検索"</string>
     <string name="suggestions_available" msgid="7189888345201419934">"検索候補は次のとおりです"</string>
-    <!-- no translation found for date_picker_title (9208721003668059792) -->
-    <skip />
-    <!-- no translation found for date_picker_headline (2846784065735639969) -->
-    <skip />
-    <!-- no translation found for date_picker_switch_to_year_selection (3412370019845183965) -->
-    <skip />
-    <!-- no translation found for date_picker_switch_to_day_selection (5820832733264067677) -->
-    <skip />
-    <!-- no translation found for date_picker_switch_to_next_month (8313783187901412102) -->
-    <skip />
-    <!-- no translation found for date_picker_switch_to_previous_month (7596294429748914881) -->
-    <skip />
+    <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="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 2d049d1..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"დიალოგი"</string>
     <string name="expanded" msgid="5974471714631304645">"გაფართოებულია"</string>
     <string name="collapsed" msgid="5389587048670450460">"ჩაკეცილი"</string>
-    <string name="dismiss" msgid="1461218791585306270">"დახურვა"</string>
-    <string name="search" msgid="8595876902241072592">"ძიება"</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 c919e76..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Диалогтік терезе"</string>
     <string name="expanded" msgid="5974471714631304645">"Жайылды"</string>
     <string name="collapsed" msgid="5389587048670450460">"Жиылды"</string>
-    <string name="dismiss" msgid="1461218791585306270">"Жабу"</string>
-    <string name="search" msgid="8595876902241072592">"Іздеу"</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 d9840e1..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ប្រអប់"</string>
     <string name="expanded" msgid="5974471714631304645">"បាន​ពង្រីក"</string>
     <string name="collapsed" msgid="5389587048670450460">"បាន​បង្រួម"</string>
-    <string name="dismiss" msgid="1461218791585306270">"ច្រានចោល"</string>
-    <string name="search" msgid="8595876902241072592">"ស្វែងរក"</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 1d0145e..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ಡೈಲಾಗ್"</string>
     <string name="expanded" msgid="5974471714631304645">"ವಿಸ್ತರಿಸಲಾಗಿದೆ"</string>
     <string name="collapsed" msgid="5389587048670450460">"ಕುಗ್ಗಿಸಲಾಗಿದೆ"</string>
-    <string name="dismiss" msgid="1461218791585306270">"ವಜಾಗೊಳಿಸಿ"</string>
-    <string name="search" msgid="8595876902241072592">"ಹುಡುಕಿ"</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 361bac5..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"대화상자"</string>
     <string name="expanded" msgid="5974471714631304645">"펼침"</string>
     <string name="collapsed" msgid="5389587048670450460">"접힘"</string>
-    <string name="dismiss" msgid="1461218791585306270">"닫기"</string>
-    <string name="search" msgid="8595876902241072592">"검색"</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 0d3bfa9..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Диалог"</string>
     <string name="expanded" msgid="5974471714631304645">"Жайылып көрсөтүлдү"</string>
     <string name="collapsed" msgid="5389587048670450460">"Жыйыштырылды"</string>
-    <string name="dismiss" msgid="1461218791585306270">"Четке кагуу"</string>
-    <string name="search" msgid="8595876902241072592">"Издөө"</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 3c06f84..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ກ່ອງໂຕ້ຕອບ"</string>
     <string name="expanded" msgid="5974471714631304645">"ຂະຫຍາຍແລ້ວ"</string>
     <string name="collapsed" msgid="5389587048670450460">"ຫຍໍ້ແລ້ວ"</string>
-    <string name="dismiss" msgid="1461218791585306270">"ປິດໄວ້"</string>
-    <string name="search" msgid="8595876902241072592">"ຊອກຫາ"</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 da08acd..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,13 +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="dismiss" msgid="1461218791585306270">"Atsisakyti"</string>
-    <string name="search" msgid="8595876902241072592">"Paieška"</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 ded3c32..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,13 +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="dismiss" msgid="1461218791585306270">"Noraidīt"</string>
-    <string name="search" msgid="8595876902241072592">"Meklēšana"</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 473d9ba..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,13 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"Дијалог"</string>
     <string name="expanded" msgid="5974471714631304645">"Проширено"</string>
     <string name="collapsed" msgid="5389587048670450460">"Собрано"</string>
-    <string name="dismiss" msgid="1461218791585306270">"Отфрли"</string>
-    <string name="search" msgid="8595876902241072592">"Пребарување"</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 894966d..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ഡയലോഗ്"</string>
     <string name="expanded" msgid="5974471714631304645">"വിപുലീകരിച്ചത്"</string>
     <string name="collapsed" msgid="5389587048670450460">"ചുരുക്കിയത്"</string>
-    <string name="dismiss" msgid="1461218791585306270">"ഡിസ്‌മിസ് ചെയ്യുക"</string>
-    <string name="search" msgid="8595876902241072592">"തിരയുക"</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 6726d22..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Харилцах цонх"</string>
     <string name="expanded" msgid="5974471714631304645">"Дэлгэсэн"</string>
     <string name="collapsed" msgid="5389587048670450460">"Хураасан"</string>
-    <string name="dismiss" msgid="1461218791585306270">"Хаах"</string>
-    <string name="search" msgid="8595876902241072592">"Хайлт"</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 843cbab..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"डायलॉग"</string>
     <string name="expanded" msgid="5974471714631304645">"विस्तारित केला"</string>
     <string name="collapsed" msgid="5389587048670450460">"कोलॅप्स केला"</string>
-    <string name="dismiss" msgid="1461218791585306270">"डिसमिस करा"</string>
-    <string name="search" msgid="8595876902241072592">"शोधा"</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 c06ad24..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,13 +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="dismiss" msgid="1461218791585306270">"Ketepikan"</string>
-    <string name="search" msgid="8595876902241072592">"Carian"</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 c9f7272..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ဒိုင်ယာလော့"</string>
     <string name="expanded" msgid="5974471714631304645">"ချဲ့ထားသည်"</string>
     <string name="collapsed" msgid="5389587048670450460">"ခေါက်ထားသည်"</string>
-    <string name="dismiss" msgid="1461218791585306270">"ပယ်ရန်"</string>
-    <string name="search" msgid="8595876902241072592">"ရှာရန်"</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 1b20120..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,13 +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="dismiss" msgid="1461218791585306270">"Lukk"</string>
-    <string name="search" msgid="8595876902241072592">"Søk"</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 c099435..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"डायलग"</string>
     <string name="expanded" msgid="5974471714631304645">"एक्स्पान्ड गरियो"</string>
     <string name="collapsed" msgid="5389587048670450460">"कोल्याप्स गरियो"</string>
-    <string name="dismiss" msgid="1461218791585306270">"हटाउनुहोस्"</string>
-    <string name="search" msgid="8595876902241072592">"खोज्नुहोस्"</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 ca7c80a..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,13 +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="dismiss" msgid="1461218791585306270">"Sluiten"</string>
-    <string name="search" msgid="8595876902241072592">"Zoeken"</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 afac4e3..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ଡାଏଲଗ"</string>
     <string name="expanded" msgid="5974471714631304645">"ବିସ୍ତାର କରାଯାଇଛି"</string>
     <string name="collapsed" msgid="5389587048670450460">"ସଙ୍କୁଚିତ କରାଯାଇଛି"</string>
-    <string name="dismiss" msgid="1461218791585306270">"ଖାରଜ କରନ୍ତୁ"</string>
-    <string name="search" msgid="8595876902241072592">"ସର୍ଚ୍ଚ କରନ୍ତୁ"</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 0728c6c..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ਵਿੰਡੋ"</string>
     <string name="expanded" msgid="5974471714631304645">"ਵਿਸਤਾਰ ਕੀਤਾ ਗਿਆ"</string>
     <string name="collapsed" msgid="5389587048670450460">"ਸਮੇਟਿਆ ਗਿਆ"</string>
-    <string name="dismiss" msgid="1461218791585306270">"ਖਾਰਜ ਕਰੋ"</string>
-    <string name="search" msgid="8595876902241072592">"ਖੋਜੋ"</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 26ee516..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,13 +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="dismiss" msgid="1461218791585306270">"Zamknij"</string>
-    <string name="search" msgid="8595876902241072592">"Szukaj"</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 339ab46..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,13 +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="dismiss" msgid="1461218791585306270">"Dispensar"</string>
-    <string name="search" msgid="8595876902241072592">"Pesquisa"</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 ab2f38d..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,13 +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="dismiss" msgid="1461218791585306270">"Ignorar"</string>
-    <string name="search" msgid="8595876902241072592">"Pesquisar"</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 339ab46..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,13 +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="dismiss" msgid="1461218791585306270">"Dispensar"</string>
-    <string name="search" msgid="8595876902241072592">"Pesquisa"</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 60335f1..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,13 +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="dismiss" msgid="1461218791585306270">"Respinge"</string>
-    <string name="search" msgid="8595876902241072592">"Caută"</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 656224f..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Диалоговое окно"</string>
     <string name="expanded" msgid="5974471714631304645">"Развернуто"</string>
     <string name="collapsed" msgid="5389587048670450460">"Свернуто"</string>
-    <string name="dismiss" msgid="1461218791585306270">"Закрыть"</string>
-    <string name="search" msgid="8595876902241072592">"Строка поиска"</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 9d042e5..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"සංවාදය"</string>
     <string name="expanded" msgid="5974471714631304645">"දිග හරින ලදි"</string>
     <string name="collapsed" msgid="5389587048670450460">"හකුළන ලදි"</string>
-    <string name="dismiss" msgid="1461218791585306270">"අස් කරන්න"</string>
-    <string name="search" msgid="8595876902241072592">"සෙවීම"</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 11cf395..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,13 +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="dismiss" msgid="1461218791585306270">"Zavrieť"</string>
-    <string name="search" msgid="8595876902241072592">"Hľadať"</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 d7deaad..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,13 +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="dismiss" msgid="1461218791585306270">"Opusti"</string>
-    <string name="search" msgid="8595876902241072592">"Iskanje"</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 6d9f00d..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,13 +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="dismiss" msgid="1461218791585306270">"Hiq"</string>
-    <string name="search" msgid="8595876902241072592">"Kërko"</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 67dba7f..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Дијалог"</string>
     <string name="expanded" msgid="5974471714631304645">"Проширено је"</string>
     <string name="collapsed" msgid="5389587048670450460">"Скупљено је"</string>
-    <string name="dismiss" msgid="1461218791585306270">"Одбаци"</string>
-    <string name="search" msgid="8595876902241072592">"Претрага"</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 43fd705..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,13 +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="dismiss" msgid="1461218791585306270">"Stäng"</string>
-    <string name="search" msgid="8595876902241072592">"Sök"</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 3c8c90e..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,13 +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="dismiss" msgid="1461218791585306270">"Ondoa"</string>
-    <string name="search" msgid="8595876902241072592">"Tafuta"</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 86098d5..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"உரையாடல்"</string>
     <string name="expanded" msgid="5974471714631304645">"விரிக்கப்பட்டது"</string>
     <string name="collapsed" msgid="5389587048670450460">"சுருக்கப்பட்டது"</string>
-    <string name="dismiss" msgid="1461218791585306270">"மூடும்"</string>
-    <string name="search" msgid="8595876902241072592">"தேடும்"</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 d3899dc..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"డైలాగ్"</string>
     <string name="expanded" msgid="5974471714631304645">"విస్తరించబడింది"</string>
     <string name="collapsed" msgid="5389587048670450460">"కుదించబడింది"</string>
-    <string name="dismiss" msgid="1461218791585306270">"విస్మరించండి"</string>
-    <string name="search" msgid="8595876902241072592">"సెర్చ్ చేయండి"</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 94a81e4..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,13 +20,26 @@
     <string name="dialog" msgid="4057925834421392736">"กล่องโต้ตอบ"</string>
     <string name="expanded" msgid="5974471714631304645">"ขยาย"</string>
     <string name="collapsed" msgid="5389587048670450460">"ยุบ"</string>
-    <string name="dismiss" msgid="1461218791585306270">"ปิด"</string>
-    <string name="search" msgid="8595876902241072592">"ค้นหา"</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 873992f..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,13 +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="dismiss" msgid="1461218791585306270">"I-dismiss"</string>
-    <string name="search" msgid="8595876902241072592">"Maghanap"</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 404c0f4..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,13 +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="dismiss" msgid="1461218791585306270">"Kapat"</string>
-    <string name="search" msgid="8595876902241072592">"Arama"</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 9e5cbc2..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"Вікно"</string>
     <string name="expanded" msgid="5974471714631304645">"Розгорнуто"</string>
     <string name="collapsed" msgid="5389587048670450460">"Згорнуто"</string>
-    <string name="dismiss" msgid="1461218791585306270">"Закрити"</string>
-    <string name="search" msgid="8595876902241072592">"Пошук"</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 8e99d91..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"ڈائلاگ"</string>
     <string name="expanded" msgid="5974471714631304645">"پھیلایا گیا"</string>
     <string name="collapsed" msgid="5389587048670450460">"سکیڑا گیا"</string>
-    <string name="dismiss" msgid="1461218791585306270">"برخاست کریں"</string>
-    <string name="search" msgid="8595876902241072592">"تلاش کریں"</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 615b41b..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,13 +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="dismiss" msgid="1461218791585306270">"Yopish"</string>
-    <string name="search" msgid="8595876902241072592">"Qidiruv"</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 6f28932..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,13 +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="dismiss" msgid="1461218791585306270">"Đóng"</string>
-    <string name="search" msgid="8595876902241072592">"Tìm kiếm"</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 f87282a..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"对话框"</string>
     <string name="expanded" msgid="5974471714631304645">"已展开"</string>
     <string name="collapsed" msgid="5389587048670450460">"已收起"</string>
-    <string name="dismiss" msgid="1461218791585306270">"关闭"</string>
-    <string name="search" msgid="8595876902241072592">"搜索"</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 8f77fd6..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"對話框"</string>
     <string name="expanded" msgid="5974471714631304645">"展開咗"</string>
     <string name="collapsed" msgid="5389587048670450460">"合埋咗"</string>
-    <string name="dismiss" msgid="1461218791585306270">"關閉"</string>
-    <string name="search" msgid="8595876902241072592">"搜尋"</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 3373d5c..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,13 +20,35 @@
     <string name="dialog" msgid="4057925834421392736">"對話方塊"</string>
     <string name="expanded" msgid="5974471714631304645">"已展開"</string>
     <string name="collapsed" msgid="5389587048670450460">"已收合"</string>
-    <string name="dismiss" msgid="1461218791585306270">"關閉"</string>
-    <string name="search" msgid="8595876902241072592">"搜尋"</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 a2276aa..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,13 +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="dismiss" msgid="1461218791585306270">"Chitha"</string>
-    <string name="search" msgid="8595876902241072592">"Sesha"</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 c75989b..cdbd37b 100644
--- a/compose/material3/material3/src/androidMain/res/values/strings.xml
+++ b/compose/material3/material3/src/androidMain/res/values/strings.xml
@@ -22,17 +22,54 @@
     <string name="expanded">Expanded</string>
     <!-- Spoken description of collapsed state of an expandable item -->
     <string name="collapsed">Collapsed</string>
-    <!-- Spoken description of a generic dismiss action item -->
-    <string name="dismiss">Dismiss</string>
+    <!-- Spoken description of a snackbar dismiss action -->
+    <string name="snackbar_dismiss">Dismiss</string>
     <!-- Spoken description of a search bar -->
-    <string name="search">Search</string>
+    <string name="search_bar_search">Search</string>
     <!-- Spoken description when search suggestions are available -->
     <string name="suggestions_available">Suggestions below</string>
     <!-- Spoken description of date picker and date input items -->
     <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 098c52c..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
@@ -79,67 +82,99 @@
 import androidx.compose.ui.draw.rotate
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
+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.
-// TODO: Update the docs to reference the upcoming DatePickerDialog.
 /**
  * <a href="https://m3.material.io/components/date-pickers/overview" class="external" target="_blank">Material Design date picker</a>.
  *
  * 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.
@@ -149,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
     )
 }
 
@@ -171,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
@@ -201,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)!! }) }
         )
     }
 }
@@ -290,6 +279,7 @@
  * Contains default values used by the date pickers.
  */
 @ExperimentalMaterial3Api
+@Stable
 object DatePickerDefaults {
 
     /**
@@ -368,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)
     }
 
     /**
@@ -414,6 +408,29 @@
 
     /** The range of years for the date picker dialogs. */
     val YearRange: IntRange = IntRange(1900, 2100)
+
+    /** The default tonal elevation used for [DatePickerDialog]. */
+    val TonalElevation: Dp = DatePickerModalTokens.ContainerElevation
+
+    /** 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"
 }
 
 /**
@@ -584,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, minHeight = ContainerHeight)
+            .sizeIn(minWidth = DatePickerModalTokens.ContainerWidth)
             .padding(DatePickerHorizontalPadding)
     ) {
         DatePickerHeader(
@@ -658,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 {
@@ -695,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
                 )
@@ -710,8 +1041,20 @@
                 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
+                        // below it.
+                        modifier = Modifier.requiredHeight(
+                            RecommendedSizeForAccessibility * (MaxCalendarRows + 1) -
+                                DividerDefaults.Thickness
+                        ),
                         onYearSelected = { year ->
                             // Switch back to the monthly calendar and scroll to the selected year.
                             yearPickerVisible = !yearPickerVisible
@@ -719,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
                                     )
@@ -727,7 +1070,7 @@
                             }
                         },
                         colors = colors,
-                        datePickerState = datePickerState
+                        stateData = stateData
                     )
                     Divider()
                 }
@@ -737,7 +1080,7 @@
 }
 
 @Composable
-private fun DatePickerHeader(
+internal fun DatePickerHeader(
     modifier: Modifier,
     title: (@Composable () -> Unit)?,
     titleContentColor: Color,
@@ -753,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()
@@ -768,7 +1113,7 @@
             ProvideTextStyle(textStyle) {
                 Row(
                     modifier = Modifier.fillMaxWidth(),
-                    horizontalArrangement = Arrangement.Start,
+                    horizontalArrangement = Arrangement.SpaceBetween,
                     verticalAlignment = Alignment.CenterVertically,
                     content = content
                 )
@@ -784,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
                 )
@@ -815,7 +1167,8 @@
                     month = month,
                     onDateSelected = onDateSelected,
                     today = today,
-                    selectedDate = datePickerState.selectedDate,
+                    selectedDate = stateData.selectedStartDate,
+                    dateFormatter = dateFormatter,
                     dateValidator = dateValidator,
                     colors = colors
                 )
@@ -827,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
                 ) {
@@ -858,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
@@ -902,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),
@@ -921,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++
                     }
                 }
             }
@@ -960,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,
@@ -980,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
@@ -999,10 +1373,7 @@
         }
     ) {
         Box(contentAlignment = Alignment.Center) {
-            Text(
-                text = text,
-                textAlign = TextAlign.Center
-            )
+            content()
         }
     }
 }
@@ -1010,51 +1381,67 @@
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun YearPicker(
+    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.
+        val containerColor = if (colors.containerColor == MaterialTheme.colorScheme.surface) {
+            MaterialTheme.colorScheme.surfaceColorAtElevation(LocalAbsoluteTonalElevation.current)
+        } else {
+            colors.containerColor
+        }
         LazyVerticalGrid(
             columns = GridCells.Fixed(YearsInRow),
-            // Keep the height the same as the monthly calendar height + weekdays height.
-            modifier = Modifier
-                .requiredHeight(RecommendedSizeForAccessibility * (MaxCalendarRows + 1))
-                .background(colors.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
                     )
                 }
@@ -1066,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,
@@ -1085,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
     ) {
@@ -1132,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) {
@@ -1211,23 +1604,10 @@
     top = 16.dp,
     bottom = 12.dp
 )
-internal val CalendarMonthSubheadPadding = PaddingValues(
-    start = 12.dp,
-    top = 20.dp,
-    bottom = 8.dp
-)
+
 private val YearsVerticalPadding = 16.dp
 
 private const val MaxCalendarRows = 6
 private const val YearsInRow: Int = 3
 
-// TODO: Remove after b/247694457 for updating the tokens is resolved.
-private val ContainerWidth = 360.dp
-private val ContainerHeight = 568.dp
-
-// 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/NavigationDrawer.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
index c66c26e3..f10ff73 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
@@ -57,9 +57,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.input.ScrollContainerInfo
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.provideScrollContainerInfo
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
@@ -266,15 +264,6 @@
 
     val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open)
     val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
-
-    val containerInfo = remember(gesturesEnabled) {
-        object : ScrollContainerInfo {
-            override fun canScrollHorizontally() = gesturesEnabled
-
-            override fun canScrollVertically() = false
-        }
-    }
-
     Box(
         modifier
             .fillMaxSize()
@@ -288,7 +277,6 @@
                 velocityThreshold = DrawerVelocityThreshold,
                 resistance = null
             )
-            .provideScrollContainerInfo(containerInfo)
     ) {
         Box {
             content()
@@ -369,14 +357,6 @@
 
     val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open)
     val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
-    val containerInfo = remember(gesturesEnabled) {
-        object : ScrollContainerInfo {
-            override fun canScrollHorizontally() = gesturesEnabled
-
-            override fun canScrollVertically() = false
-        }
-    }
-
     Box(
         modifier.swipeable(
             state = drawerState.swipeableState,
@@ -387,7 +367,7 @@
             reverseDirection = isRtl,
             velocityThreshold = DrawerVelocityThreshold,
             resistance = null
-        ).provideScrollContainerInfo(containerInfo)
+        )
     ) {
         Layout(content = {
             Box(Modifier.semantics {
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 c56363f..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
@@ -39,14 +39,10 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.layout.BoxWithConstraints
 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.heightIn
-import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.requiredSizeIn
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.widthIn
@@ -66,7 +62,6 @@
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.draw.shadow
@@ -84,14 +79,12 @@
 import androidx.compose.ui.input.pointer.positionChange
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.layoutId
-import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.disabled
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.setProgress
-import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
@@ -150,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,
@@ -164,30 +161,31 @@
     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,
-        thumb = remember(interactionSource, colors, enabled) { {
+        onValueChange = onValueChange,
+        onValueChangeFinished = onValueChangeFinished,
+        steps = steps,
+        value = value,
+        valueRange = valueRange,
+        thumb = {
             SliderDefaults.Thumb(
                 interactionSource = interactionSource,
                 colors = colors,
                 enabled = enabled
             )
-        } },
-        track = remember(colors, enabled) { { sliderPositions ->
+        },
+        track = { sliderPositions ->
             SliderDefaults.Track(
                 colors = colors,
                 enabled = enabled,
                 sliderPositions = sliderPositions
             )
-        } }
+        }
     )
 }
 
@@ -231,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(
@@ -246,24 +249,25 @@
     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 = remember(colors, enabled) { { sliderPositions ->
+        track = { sliderPositions ->
             SliderDefaults.Track(
                 colors = colors,
                 enabled = enabled,
                 sliderPositions = sliderPositions
             )
-        } }
+        }
     )
 }
 
@@ -308,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(
@@ -322,14 +331,13 @@
     onValueChangeFinished: (() -> Unit)? = null,
     colors: SliderColors = SliderDefaults.colors(),
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    thumb: @Composable (SliderPositions) -> Unit =
-        remember(interactionSource, colors, enabled) { {
-            SliderDefaults.Thumb(
-                interactionSource = interactionSource,
-                colors = colors,
-                enabled = enabled
-            )
-        } }
+    thumb: @Composable (SliderPositions) -> Unit = {
+        SliderDefaults.Thumb(
+            interactionSource = interactionSource,
+            colors = colors,
+            enabled = enabled
+        )
+    }
 ) {
     require(steps >= 0) { "steps should be >= 0" }
 
@@ -348,6 +356,104 @@
 }
 
 /**
+ * <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.
@@ -380,6 +486,11 @@
  * @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(
@@ -397,404 +508,151 @@
     val endInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
 
     require(steps >= 0) { "steps should be >= 0" }
-    val onValueChangeState = rememberUpdatedState<(ClosedFloatingPointRange<Float>) -> Unit> {
-        if (it != value) {
-            onValueChange(it)
+
+    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
+            )
         }
-    }
-    val tickFractions = remember(steps) {
-        stepsToTickFractions(steps)
-    }
-
-    BoxWithConstraints(
-        modifier = modifier
-            .minimumInteractiveComponentSize()
-            .requiredSizeIn(minWidth = ThumbWidth * 2, minHeight = ThumbHeight * 2)
-    ) {
-        val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
-        val widthPx = constraints.maxWidth.toFloat()
-        val maxPx: Float
-        val minPx: Float
-
-        with(LocalDensity.current) {
-            maxPx = widthPx - ThumbWidth.toPx() / 2
-            minPx = ThumbWidth.toPx() / 2
-        }
-
-        fun scaleToUserValue(offset: ClosedFloatingPointRange<Float>) =
-            scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive)
-
-        fun scaleToOffset(userValue: Float) =
-            scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx)
-
-        val rawOffsetStart = remember { mutableStateOf(scaleToOffset(value.start)) }
-        val rawOffsetEnd = remember { mutableStateOf(scaleToOffset(value.endInclusive)) }
-
-        val gestureEndAction = rememberUpdatedState<(Boolean) -> Unit> {
-            onValueChangeFinished?.invoke()
-        }
-
-        val onDrag = rememberUpdatedState<(Boolean, Float) -> Unit> { isStart, offset ->
-            val offsetRange = if (isStart) {
-                rawOffsetStart.value = (rawOffsetStart.value + offset)
-                rawOffsetEnd.value = scaleToOffset(value.endInclusive)
-                val offsetEnd = rawOffsetEnd.value
-                var offsetStart = rawOffsetStart.value.coerceIn(minPx, offsetEnd)
-                offsetStart = snapValueToTick(offsetStart, tickFractions, minPx, maxPx)
-                offsetStart..offsetEnd
-            } else {
-                rawOffsetEnd.value = (rawOffsetEnd.value + offset)
-                rawOffsetStart.value = scaleToOffset(value.start)
-                val offsetStart = rawOffsetStart.value
-                var offsetEnd = rawOffsetEnd.value.coerceIn(offsetStart, maxPx)
-                offsetEnd = snapValueToTick(offsetEnd, tickFractions, minPx, maxPx)
-                offsetStart..offsetEnd
-            }
-
-            onValueChangeState.value.invoke(scaleToUserValue(offsetRange))
-        }
-
-        val pressDrag = Modifier.rangeSliderPressDragModifier(
-            startInteractionSource,
-            endInteractionSource,
-            rawOffsetStart,
-            rawOffsetEnd,
-            enabled,
-            isRtl,
-            widthPx,
-            valueRange,
-            gestureEndAction,
-            onDrag,
-        )
-        // The positions of the thumbs are dependant on each other.
-        val coercedStart = value.start.coerceIn(valueRange.start, value.endInclusive)
-        val coercedEnd = value.endInclusive.coerceIn(value.start, valueRange.endInclusive)
-        val fractionStart = calcFraction(valueRange.start, valueRange.endInclusive, coercedStart)
-        val fractionEnd = calcFraction(valueRange.start, valueRange.endInclusive, coercedEnd)
-        val startSteps = floor(steps * fractionEnd).toInt()
-        val endSteps = floor(steps * (1f - fractionStart)).toInt()
-
-        val startThumbSemantics = Modifier.sliderSemantics(
-            coercedStart,
-            enabled,
-            { value -> onValueChangeState.value.invoke(value..coercedEnd) },
-            onValueChangeFinished,
-            valueRange.start..coercedEnd,
-            startSteps
-        )
-        val endThumbSemantics = Modifier.sliderSemantics(
-            coercedEnd,
-            enabled,
-            { value -> onValueChangeState.value.invoke(coercedStart..value) },
-            onValueChangeFinished,
-            coercedStart..valueRange.endInclusive,
-            endSteps
-        )
-
-        RangeSliderImpl(
-            enabled,
-            fractionStart,
-            fractionEnd,
-            tickFractions,
-            colors,
-            maxPx - minPx,
-            startInteractionSource,
-            endInteractionSource,
-            modifier = pressDrag,
-            startThumbSemantics,
-            endThumbSemantics
-        )
-    }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun RangeSliderImpl(
-    enabled: Boolean,
-    positionFractionStart: Float,
-    positionFractionEnd: Float,
-    tickFractions: FloatArray,
-    colors: SliderColors,
-    width: Float,
-    startInteractionSource: MutableInteractionSource,
-    endInteractionSource: MutableInteractionSource,
-    modifier: Modifier,
-    startThumbSemantics: Modifier,
-    endThumbSemantics: Modifier
-) {
-    val startContentDescription = getString(Strings.SliderRangeStart)
-    val endContentDescription = getString(Strings.SliderRangeEnd)
-    Box(modifier.then(DefaultSliderConstraints)) {
-        val trackStrokeWidth: Float
-        val widthDp: Dp
-        with(LocalDensity.current) {
-            trackStrokeWidth = TrackHeight.toPx()
-            widthDp = width.toDp()
-        }
-
-        val offsetStart = widthDp * positionFractionStart
-        val offsetEnd = widthDp * positionFractionEnd
-
-        TempRangeSliderTrack(
-            Modifier
-                .align(Alignment.CenterStart)
-                .fillMaxSize(),
-            colors,
-            enabled,
-            positionFractionStart,
-            positionFractionEnd,
-            tickFractions,
-            ThumbWidth,
-            trackStrokeWidth
-        )
-
-        TempRangeSliderThumb(
-            offset = offsetStart,
-            content = {
-                SliderDefaults.Thumb(
-                    modifier = Modifier
-                        .semantics(mergeDescendants = true) {
-                            contentDescription = startContentDescription
-                        }
-                        .focusable(true, startInteractionSource)
-                        .then(startThumbSemantics),
-                    colors = colors,
-                    enabled = enabled,
-                    interactionSource = startInteractionSource
-                )
-            }
-        )
-        TempRangeSliderThumb(
-            offset = offsetEnd,
-            content = {
-                SliderDefaults.Thumb(
-                    modifier = Modifier
-                        .semantics(mergeDescendants = true) {
-                            contentDescription = endContentDescription
-                        }
-                        .focusable(true, endInteractionSource)
-                        .then(endThumbSemantics),
-                    colors = colors,
-                    enabled = enabled,
-                    interactionSource = endInteractionSource
-                )
-            }
-        )
-    }
+    )
 }
 
 /**
- * Object to hold defaults used by [Slider]
+ * <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.
+ *
+ * It uses the provided startThumb for the slider's start thumb and endThumb for the
+ * slider's end thumb. It also uses the provided track for the slider's track. If nothing is
+ * passed for these parameters, it will use [SliderDefaults.Thumb] and [SliderDefaults.Track]
+ * for the thumbs and track.
+ *
+ * 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
+ *
+ * A custom start/end thumb and track can be provided:
+ *
+ * @sample androidx.compose.material3.samples.RangeSliderWithCustomComponents
+ *
+ * @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.
+ * @param startInteractionSource the [MutableInteractionSource] representing the stream of
+ * [Interaction]s for the start thumb. You can create and pass in your own
+ * `remember`ed instance to observe.
+ * @param endInteractionSource the [MutableInteractionSource] representing the stream of
+ * [Interaction]s for the end thumb. You can create and pass in your own
+ * `remember`ed instance to observe.
+ * @param startThumb the start thumb to be displayed on the Range Slider. The lambda receives a
+ * [SliderPositions] which is used to obtain the current active track and the tick
+ * positions if the range slider is discrete.
+ * @param endThumb the end thumb to be displayed on the Range Slider. The lambda receives a
+ * [SliderPositions] which is used to obtain the current active track and the tick
+ * positions if the range slider is discrete.
+ * @param track the track to be displayed on the range 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 range slider is discrete.
  */
-object SliderDefaults {
-
-    /**
-     * Creates a [SliderColors] that represents the different colors used in parts of the
-     * [Slider] in different states.
-     *
-     * For the name references below the words "active" and "inactive" are used. Active part of
-     * the slider is filled with progress, so if slider's progress is 30% out of 100%, left (or
-     * right in RTL) 30% of the track will be active, while the rest is inactive.
-     *
-     * @param thumbColor thumb color when enabled
-     * @param activeTrackColor color of the track in the part that is "active", meaning that the
-     * thumb is ahead of it
-     * @param activeTickColor colors to be used to draw tick marks on the active track, if `steps`
-     * is specified
-     * @param inactiveTrackColor color of the track in the part that is "inactive", meaning that the
-     * thumb is before it
-     * @param inactiveTickColor colors to be used to draw tick marks on the inactive track, if
-     * `steps` are specified on the Slider is specified
-     * @param disabledThumbColor thumb colors when disabled
-     * @param disabledActiveTrackColor color of the track in the "active" part when the Slider is
-     * disabled
-     * @param disabledActiveTickColor colors to be used to draw tick marks on the active track
-     * when Slider is disabled and when `steps` are specified on it
-     * @param disabledInactiveTrackColor color of the track in the "inactive" part when the
-     * Slider is disabled
-     * @param disabledInactiveTickColor colors to be used to draw tick marks on the inactive part
-     * of the track when Slider is disabled and when `steps` are specified on it
-     */
-    @Composable
-    fun colors(
-        thumbColor: Color = SliderTokens.HandleColor.toColor(),
-        activeTrackColor: Color = SliderTokens.ActiveTrackColor.toColor(),
-        activeTickColor: Color = SliderTokens.TickMarksActiveContainerColor
-            .toColor()
-            .copy(alpha = SliderTokens.TickMarksActiveContainerOpacity),
-        inactiveTrackColor: Color = SliderTokens.InactiveTrackColor.toColor(),
-        inactiveTickColor: Color = SliderTokens.TickMarksInactiveContainerColor.toColor()
-            .copy(alpha = SliderTokens.TickMarksInactiveContainerOpacity),
-        disabledThumbColor: Color = SliderTokens.DisabledHandleColor
-            .toColor()
-            .copy(alpha = SliderTokens.DisabledHandleOpacity)
-            .compositeOver(MaterialTheme.colorScheme.surface),
-        disabledActiveTrackColor: Color =
-            SliderTokens.DisabledActiveTrackColor
-                .toColor()
-                .copy(alpha = SliderTokens.DisabledActiveTrackOpacity),
-        disabledActiveTickColor: Color = SliderTokens.TickMarksDisabledContainerColor
-            .toColor()
-            .copy(alpha = SliderTokens.TickMarksDisabledContainerOpacity),
-        disabledInactiveTrackColor: Color =
-            SliderTokens.DisabledInactiveTrackColor
-                .toColor()
-                .copy(alpha = SliderTokens.DisabledInactiveTrackOpacity),
-
-        disabledInactiveTickColor: Color = SliderTokens.TickMarksDisabledContainerColor.toColor()
-            .copy(alpha = SliderTokens.TickMarksDisabledContainerOpacity)
-    ): SliderColors = SliderColors(
-        thumbColor = thumbColor,
-        activeTrackColor = activeTrackColor,
-        activeTickColor = activeTickColor,
-        inactiveTrackColor = inactiveTrackColor,
-        inactiveTickColor = inactiveTickColor,
-        disabledThumbColor = disabledThumbColor,
-        disabledActiveTrackColor = disabledActiveTrackColor,
-        disabledActiveTickColor = disabledActiveTickColor,
-        disabledInactiveTrackColor = disabledInactiveTrackColor,
-        disabledInactiveTickColor = disabledInactiveTickColor
-    )
-
-    /**
-     * The Default thumb for [Slider] and [RangeSlider]
-     *
-     * @param interactionSource the [MutableInteractionSource] representing the stream of
-     * [Interaction]s for this thumb. You can create and pass in your own `remember`ed
-     * instance to observe
-     * @param modifier the [Modifier] to be applied to the thumb.
-     * @param colors [SliderColors] that will be used to resolve the colors used for this thumb in
-     * different states. See [SliderDefaults.colors].
-     * @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.
-     */
-    @Composable
-    @ExperimentalMaterial3Api
-    fun Thumb(
-        interactionSource: MutableInteractionSource,
-        modifier: Modifier = Modifier,
-        colors: SliderColors = colors(),
-        enabled: Boolean = true,
-        thumbSize: DpSize = ThumbSize
-    ) {
-        val interactions = remember { mutableStateListOf<Interaction>() }
-        LaunchedEffect(interactionSource) {
-            interactionSource.interactions.collect { interaction ->
-                when (interaction) {
-                    is PressInteraction.Press -> interactions.add(interaction)
-                    is PressInteraction.Release -> interactions.remove(interaction.press)
-                    is PressInteraction.Cancel -> interactions.remove(interaction.press)
-                    is DragInteraction.Start -> interactions.add(interaction)
-                    is DragInteraction.Stop -> interactions.remove(interaction.start)
-                    is DragInteraction.Cancel -> interactions.remove(interaction.start)
-                }
-            }
-        }
-
-        val elevation = if (interactions.isNotEmpty()) {
-            ThumbPressedElevation
-        } else {
-            ThumbDefaultElevation
-        }
-        val shape = SliderTokens.HandleShape.toShape()
-
-        Spacer(
-            modifier
-                .size(thumbSize)
-                .indication(
-                    interactionSource = interactionSource,
-                    indication = rememberRipple(
-                        bounded = false,
-                        radius = SliderTokens.StateLayerSize / 2
-                    )
-                )
-                .hoverable(interactionSource = interactionSource)
-                .shadow(if (enabled) elevation else 0.dp, shape, clip = false)
-                .background(colors.thumbColor(enabled).value, shape)
+@Composable
+fun RangeSlider(
+    value: ClosedFloatingPointRange<Float>,
+    onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
+    onValueChangeFinished: (() -> Unit)? = null,
+    colors: SliderColors = SliderDefaults.colors(),
+    startInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    endInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    startThumb: @Composable (SliderPositions) -> Unit = {
+        SliderDefaults.Thumb(
+            interactionSource = startInteractionSource,
+            colors = colors,
+            enabled = enabled
         )
-    }
+    },
+    endThumb: @Composable (SliderPositions) -> Unit = {
+        SliderDefaults.Thumb(
+            interactionSource = endInteractionSource,
+            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" }
 
-    /**
-     * The Default track for [Slider] and [RangeSlider]
-     *
-     * @param modifier the [Modifier] to be applied to the track.
-     * @param colors [SliderColors] that will be used to resolve the colors used for this track in
-     * different states. See [SliderDefaults.colors].
-     * @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 sliderPositions [SliderPositions] which is used to obtain the current active track
-     * and the tick positions if the slider is discrete.
-     */
-    @Composable
-    @ExperimentalMaterial3Api
-    fun Track(
-        sliderPositions: SliderPositions,
-        modifier: Modifier = Modifier,
-        colors: SliderColors = colors(),
-        enabled: Boolean = true,
-    ) {
-        val inactiveTrackColor = colors.trackColor(enabled, active = false)
-        val activeTrackColor = colors.trackColor(enabled, active = true)
-        val inactiveTickColor = colors.tickColor(enabled, active = false)
-        val activeTickColor = colors.tickColor(enabled, active = true)
-        Canvas(modifier
-            .fillMaxWidth()
-            .height(TrackHeight)
-        ) {
-            val isRtl = layoutDirection == LayoutDirection.Rtl
-            val sliderLeft = Offset(0f, center.y)
-            val sliderRight = Offset(size.width, center.y)
-            val sliderStart = if (isRtl) sliderRight else sliderLeft
-            val sliderEnd = if (isRtl) sliderLeft else sliderRight
-            val tickSize = TickSize.toPx()
-            val trackStrokeWidth = TrackHeight.toPx()
-            drawLine(
-                inactiveTrackColor.value,
-                sliderStart,
-                sliderEnd,
-                trackStrokeWidth,
-                StrokeCap.Round
-            )
-            val sliderValueEnd = Offset(
-                sliderStart.x +
-                    (sliderEnd.x - sliderStart.x) * sliderPositions.positionFraction,
-                center.y
-            )
-
-            val sliderValueStart = Offset(
-                sliderStart.x +
-                    (sliderEnd.x - sliderStart.x) * 0f,
-                center.y
-            )
-
-            drawLine(
-                activeTrackColor.value,
-                sliderValueStart,
-                sliderValueEnd,
-                trackStrokeWidth,
-                StrokeCap.Round
-            )
-            sliderPositions.tickFractions.groupBy {
-                it > sliderPositions.positionFraction ||
-                    it < 0f
-            }.forEach { (outsideFraction, list) ->
-                    drawPoints(
-                        list.map {
-                            Offset(lerp(sliderStart, sliderEnd, it).x, center.y)
-                        },
-                        PointMode.Points,
-                        (if (outsideFraction) inactiveTickColor else activeTickColor).value,
-                        tickSize,
-                        StrokeCap.Round
-                    )
-                }
-        }
-    }
+    RangeSliderImpl(
+        modifier = modifier,
+        value = value,
+        onValueChange = onValueChange,
+        enabled = enabled,
+        valueRange = valueRange,
+        steps = steps,
+        onValueChangeFinished = onValueChangeFinished,
+        startInteractionSource = startInteractionSource,
+        endInteractionSource = endInteractionSource,
+        startThumb = startThumb,
+        endThumb = endThumb,
+        track = track
+    )
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 private fun SliderImpl(
     modifier: Modifier,
@@ -833,8 +691,10 @@
     val coerced = value.coerceIn(valueRange.start, valueRange.endInclusive)
 
     val positionFraction = calcFraction(valueRange.start, valueRange.endInclusive, coerced)
-    val sliderPositions = remember { SliderPositions(positionFraction, tickFractions) }
-    sliderPositions.positionFraction = positionFraction
+    val sliderPositions = remember {
+        SliderPositions(0f..positionFraction, tickFractions)
+    }
+    sliderPositions.activeRange = 0f..positionFraction
     sliderPositions.tickFractions = tickFractions
 
     val draggableState = remember(valueRange) {
@@ -942,84 +802,453 @@
     }
 }
 
-// TODO: Remove during b/242600007
 @Composable
-private fun BoxScope.TempRangeSliderThumb(
-    offset: Dp,
-    content: @Composable BoxScope.() -> Unit
+private fun RangeSliderImpl(
+    modifier: Modifier,
+    value: ClosedFloatingPointRange<Float>,
+    onValueChange: (ClosedFloatingPointRange<Float>) -> Unit,
+    enabled: Boolean,
+    valueRange: ClosedFloatingPointRange<Float>,
+    steps: Int = 0,
+    onValueChangeFinished: (() -> Unit)?,
+    startInteractionSource: MutableInteractionSource,
+    endInteractionSource: MutableInteractionSource,
+    startThumb: @Composable ((SliderPositions) -> Unit),
+    endThumb: @Composable ((SliderPositions) -> Unit),
+    track: @Composable ((SliderPositions) -> Unit)
 ) {
-    Box(
-        Modifier
-            .padding(start = offset)
-            .align(Alignment.CenterStart),
-        content = content
+    val onValueChangeState = rememberUpdatedState<(ClosedFloatingPointRange<Float>) -> Unit> {
+        if (it != value) {
+            onValueChange(it)
+        }
+    }
+
+    val tickFractions = remember(steps) {
+        stepsToTickFractions(steps)
+    }
+
+    var startThumbWidth by remember { mutableStateOf(ThumbWidth.value) }
+    var endThumbWidth by remember { mutableStateOf(ThumbWidth.value) }
+    var totalWidth by remember { mutableStateOf(0) }
+
+    val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+
+    // scales range offset from within minPx..maxPx to within valueRange.start..valueRange.end
+    fun scaleToUserValue(minPx: Float, maxPx: Float, offset: ClosedFloatingPointRange<Float>) =
+        scale(minPx, maxPx, offset, valueRange.start, valueRange.endInclusive)
+
+    // scales float userValue within valueRange.start..valueRange.end to within minPx..maxPx
+    fun scaleToOffset(minPx: Float, maxPx: Float, userValue: Float) =
+        scale(valueRange.start, valueRange.endInclusive, userValue, minPx, maxPx)
+
+    var obtainedMeasurements = remember { mutableStateOf(false) }
+    val rawOffsetStart = remember { mutableStateOf(0f) }
+    val rawOffsetEnd = remember { mutableStateOf(0f) }
+
+    val gestureEndAction = rememberUpdatedState<(Boolean) -> Unit> {
+        onValueChangeFinished?.invoke()
+    }
+
+    val onDrag = rememberUpdatedState<(Boolean, Float) -> Unit> { isStart, offset ->
+        val maxPx = max(totalWidth - endThumbWidth / 2, 0f)
+        val minPx = min(startThumbWidth / 2, maxPx)
+        val offsetRange = if (isStart) {
+            rawOffsetStart.value = (rawOffsetStart.value + offset)
+            rawOffsetEnd.value = scaleToOffset(minPx, maxPx, value.endInclusive)
+            val offsetEnd = rawOffsetEnd.value
+            var offsetStart = rawOffsetStart.value.coerceIn(minPx, offsetEnd)
+            offsetStart = snapValueToTick(offsetStart, tickFractions, minPx, maxPx)
+            offsetStart..offsetEnd
+        } else {
+            rawOffsetEnd.value = (rawOffsetEnd.value + offset)
+            rawOffsetStart.value = scaleToOffset(minPx, maxPx, value.start)
+            val offsetStart = rawOffsetStart.value
+            var offsetEnd = rawOffsetEnd.value.coerceIn(offsetStart, maxPx)
+            offsetEnd = snapValueToTick(offsetEnd, tickFractions, minPx, maxPx)
+            offsetStart..offsetEnd
+        }
+
+        onValueChangeState.value.invoke(scaleToUserValue(minPx, maxPx, offsetRange))
+    }
+
+    val pressDrag = Modifier.rangeSliderPressDragModifier(
+        startInteractionSource,
+        endInteractionSource,
+        rawOffsetStart,
+        rawOffsetEnd,
+        enabled,
+        isRtl,
+        totalWidth,
+        valueRange,
+        gestureEndAction,
+        onDrag,
     )
+
+    // The positions of the thumbs are dependant on each other.
+    val coercedStart = value.start.coerceIn(valueRange.start, value.endInclusive)
+    val coercedEnd = value.endInclusive.coerceIn(value.start, valueRange.endInclusive)
+    val positionFractionStart = calcFraction(
+        valueRange.start,
+        valueRange.endInclusive,
+        coercedStart
+    )
+    val positionFractionEnd = calcFraction(valueRange.start, valueRange.endInclusive, coercedEnd)
+
+    val sliderPositions = remember {
+        SliderPositions(
+            positionFractionStart..positionFractionEnd,
+            tickFractions
+        )
+    }
+    sliderPositions.activeRange = positionFractionStart..positionFractionEnd
+    sliderPositions.tickFractions = tickFractions
+
+    val startSteps = floor(steps * positionFractionEnd).toInt()
+    val endSteps = floor(steps * (1f - positionFractionStart)).toInt()
+
+    val startThumbSemantics = Modifier.sliderSemantics(
+        coercedStart,
+        enabled,
+        { changedVal -> onValueChangeState.value.invoke(changedVal..coercedEnd) },
+        onValueChangeFinished,
+        valueRange.start..coercedEnd,
+        startSteps
+    )
+    val endThumbSemantics = Modifier.sliderSemantics(
+        coercedEnd,
+        enabled,
+        { changedVal -> onValueChangeState.value.invoke(coercedStart..changedVal) },
+        onValueChangeFinished,
+        coercedStart..valueRange.endInclusive,
+        endSteps
+    )
+
+    val startContentDescription = getString(Strings.SliderRangeStart)
+    val endContentDescription = getString(Strings.SliderRangeEnd)
+
+    Layout(
+        {
+            Box(modifier = Modifier
+                .layoutId(RangeSliderComponents.STARTTHUMB)
+                .semantics(mergeDescendants = true) {
+                    contentDescription = startContentDescription
+                }
+                .focusable(enabled, startInteractionSource)
+                .then(startThumbSemantics)
+            ) { startThumb(sliderPositions) }
+            Box(modifier = Modifier
+                .layoutId(RangeSliderComponents.ENDTHUMB)
+                .semantics(mergeDescendants = true) {
+                    contentDescription = endContentDescription
+                }
+                .focusable(enabled, endInteractionSource)
+                .then(endThumbSemantics)
+            ) { endThumb(sliderPositions) }
+            Box(modifier = Modifier.layoutId(RangeSliderComponents.TRACK)) {
+                track(sliderPositions)
+            }
+        },
+        modifier = modifier
+            .minimumInteractiveComponentSize()
+            .requiredSizeIn(
+                minWidth = SliderTokens.HandleWidth,
+                minHeight = SliderTokens.HandleHeight
+            )
+            .then(pressDrag)
+    ) { measurables, constraints ->
+        val startThumbPlaceable = measurables.first {
+            it.layoutId == RangeSliderComponents.STARTTHUMB
+        }.measure(
+            constraints
+        )
+
+        val endThumbPlaceable = measurables.first {
+            it.layoutId == RangeSliderComponents.ENDTHUMB
+        }.measure(
+            constraints
+        )
+
+        val maxTrackWidth =
+            constraints.maxWidth - (startThumbPlaceable.width + endThumbPlaceable.width) / 2
+        val trackPlaceable = measurables.first {
+            it.layoutId == RangeSliderComponents.TRACK
+        }.measure(
+            constraints.copy(
+                minWidth = 0,
+                maxWidth = maxTrackWidth,
+                minHeight = 0
+            )
+        )
+
+        val sliderWidth = trackPlaceable.width +
+            (startThumbPlaceable.width + endThumbPlaceable.width) / 2
+        val sliderHeight = maxOf(
+            trackPlaceable.height,
+            startThumbPlaceable.height,
+            endThumbPlaceable.height
+        )
+
+        startThumbWidth = startThumbPlaceable.width.toFloat()
+        endThumbWidth = endThumbPlaceable.width.toFloat()
+        totalWidth = sliderWidth
+
+        // Updates rawOffsetStart and rawOffsetEnd with the correct min and max pixel.
+        // We use this `obtainedMeasurements` boolean so that we only do this update once.
+        // Is there a cleaner way to do this?
+        if (!obtainedMeasurements.value) {
+            val finalizedMaxPx = max(totalWidth - endThumbWidth / 2, 0f)
+            val finalizedMinPx = min(startThumbWidth / 2, finalizedMaxPx)
+            rawOffsetStart.value = scaleToOffset(
+                finalizedMinPx,
+                finalizedMaxPx,
+                value.start
+            )
+            rawOffsetEnd.value = scaleToOffset(
+                finalizedMinPx,
+                finalizedMaxPx,
+                value.endInclusive
+            )
+            obtainedMeasurements.value = true
+        }
+
+        val trackOffsetX = startThumbPlaceable.width / 2
+        val startThumbOffsetX = (trackPlaceable.width * positionFractionStart).roundToInt()
+        // When start thumb and end thumb have different widths,
+        // we need to add a correction for the centering of the slider.
+        val endCorrection = (startThumbWidth - endThumbWidth) / 2
+        val endThumbOffsetX =
+            (trackPlaceable.width * positionFractionEnd + endCorrection).roundToInt()
+        val trackOffsetY = (sliderHeight - trackPlaceable.height) / 2
+        val startThumbOffsetY = (sliderHeight - startThumbPlaceable.height) / 2
+        val endThumbOffsetY = (sliderHeight - endThumbPlaceable.height) / 2
+
+        layout(
+            sliderWidth,
+            sliderHeight
+        ) {
+            trackPlaceable.placeRelative(
+                trackOffsetX,
+                trackOffsetY
+            )
+            startThumbPlaceable.placeRelative(
+                startThumbOffsetX,
+                startThumbOffsetY
+            )
+            endThumbPlaceable.placeRelative(
+                endThumbOffsetX,
+                endThumbOffsetY
+            )
+        }
+    }
 }
 
-// TODO: Remove during b/242600007
-@Composable
-private fun TempRangeSliderTrack(
-    modifier: Modifier,
-    colors: SliderColors,
-    enabled: Boolean,
-    positionFractionStart: Float,
-    positionFractionEnd: Float,
-    tickFractions: FloatArray,
-    thumbWidth: Dp,
-    trackStrokeWidth: Float
-) {
-    val thumbRadiusPx: Float
-    val tickSize: Float
-    with(LocalDensity.current) {
-        thumbRadiusPx = thumbWidth.toPx() / 2
-        tickSize = TickSize.toPx()
-    }
-    val inactiveTrackColor = colors.trackColor(enabled, active = false)
-    val activeTrackColor = colors.trackColor(enabled, active = true)
-    val inactiveTickColor = colors.tickColor(enabled, active = false)
-    val activeTickColor = colors.tickColor(enabled, active = true)
-    Canvas(modifier) {
-        val isRtl = layoutDirection == LayoutDirection.Rtl
-        val sliderLeft = Offset(thumbRadiusPx, center.y)
-        val sliderRight = Offset(size.width - thumbRadiusPx, center.y)
-        val sliderStart = if (isRtl) sliderRight else sliderLeft
-        val sliderEnd = if (isRtl) sliderLeft else sliderRight
-        drawLine(
-            inactiveTrackColor.value,
-            sliderStart,
-            sliderEnd,
-            trackStrokeWidth,
-            StrokeCap.Round
-        )
-        val sliderValueEnd = Offset(
-            sliderStart.x + (sliderEnd.x - sliderStart.x) * positionFractionEnd,
-            center.y
-        )
+/**
+ * Object to hold defaults used by [Slider]
+ */
+@Stable
+object SliderDefaults {
 
-        val sliderValueStart = Offset(
-            sliderStart.x + (sliderEnd.x - sliderStart.x) * positionFractionStart,
-            center.y
-        )
+    /**
+     * Creates a [SliderColors] that represents the different colors used in parts of the
+     * [Slider] in different states.
+     *
+     * For the name references below the words "active" and "inactive" are used. Active part of
+     * the slider is filled with progress, so if slider's progress is 30% out of 100%, left (or
+     * right in RTL) 30% of the track will be active, while the rest is inactive.
+     *
+     * @param thumbColor thumb color when enabled
+     * @param activeTrackColor color of the track in the part that is "active", meaning that the
+     * thumb is ahead of it
+     * @param activeTickColor colors to be used to draw tick marks on the active track, if `steps`
+     * is specified
+     * @param inactiveTrackColor color of the track in the part that is "inactive", meaning that the
+     * thumb is before it
+     * @param inactiveTickColor colors to be used to draw tick marks on the inactive track, if
+     * `steps` are specified on the Slider is specified
+     * @param disabledThumbColor thumb colors when disabled
+     * @param disabledActiveTrackColor color of the track in the "active" part when the Slider is
+     * disabled
+     * @param disabledActiveTickColor colors to be used to draw tick marks on the active track
+     * when Slider is disabled and when `steps` are specified on it
+     * @param disabledInactiveTrackColor color of the track in the "inactive" part when the
+     * Slider is disabled
+     * @param disabledInactiveTickColor colors to be used to draw tick marks on the inactive part
+     * of the track when Slider is disabled and when `steps` are specified on it
+     */
+    @Composable
+    fun colors(
+        thumbColor: Color = SliderTokens.HandleColor.toColor(),
+        activeTrackColor: Color = SliderTokens.ActiveTrackColor.toColor(),
+        activeTickColor: Color = SliderTokens.TickMarksActiveContainerColor
+            .toColor()
+            .copy(alpha = SliderTokens.TickMarksActiveContainerOpacity),
+        inactiveTrackColor: Color = SliderTokens.InactiveTrackColor.toColor(),
+        inactiveTickColor: Color = SliderTokens.TickMarksInactiveContainerColor.toColor()
+            .copy(alpha = SliderTokens.TickMarksInactiveContainerOpacity),
+        disabledThumbColor: Color = SliderTokens.DisabledHandleColor
+            .toColor()
+            .copy(alpha = SliderTokens.DisabledHandleOpacity)
+            .compositeOver(MaterialTheme.colorScheme.surface),
+        disabledActiveTrackColor: Color =
+            SliderTokens.DisabledActiveTrackColor
+                .toColor()
+                .copy(alpha = SliderTokens.DisabledActiveTrackOpacity),
+        disabledActiveTickColor: Color = SliderTokens.TickMarksDisabledContainerColor
+            .toColor()
+            .copy(alpha = SliderTokens.TickMarksDisabledContainerOpacity),
+        disabledInactiveTrackColor: Color =
+            SliderTokens.DisabledInactiveTrackColor
+                .toColor()
+                .copy(alpha = SliderTokens.DisabledInactiveTrackOpacity),
 
-        drawLine(
-            activeTrackColor.value,
-            sliderValueStart,
-            sliderValueEnd,
-            trackStrokeWidth,
-            StrokeCap.Round
-        )
-        tickFractions.groupBy { it > positionFractionEnd || it < positionFractionStart }
-            .forEach { (outsideFraction, list) ->
-                drawPoints(
-                    list.map {
-                        Offset(lerp(sliderStart, sliderEnd, it).x, center.y)
-                    },
-                    PointMode.Points,
-                    (if (outsideFraction) inactiveTickColor else activeTickColor).value,
-                    tickSize,
-                    StrokeCap.Round
-                )
+        disabledInactiveTickColor: Color = SliderTokens.TickMarksDisabledContainerColor.toColor()
+            .copy(alpha = SliderTokens.TickMarksDisabledContainerOpacity)
+    ): SliderColors = SliderColors(
+        thumbColor = thumbColor,
+        activeTrackColor = activeTrackColor,
+        activeTickColor = activeTickColor,
+        inactiveTrackColor = inactiveTrackColor,
+        inactiveTickColor = inactiveTickColor,
+        disabledThumbColor = disabledThumbColor,
+        disabledActiveTrackColor = disabledActiveTrackColor,
+        disabledActiveTickColor = disabledActiveTickColor,
+        disabledInactiveTrackColor = disabledInactiveTrackColor,
+        disabledInactiveTickColor = disabledInactiveTickColor
+    )
+
+    /**
+     * The Default thumb for [Slider] and [RangeSlider]
+     *
+     * @param interactionSource the [MutableInteractionSource] representing the stream of
+     * [Interaction]s for this thumb. You can create and pass in your own `remember`ed
+     * instance to observe
+     * @param modifier the [Modifier] to be applied to the thumb.
+     * @param colors [SliderColors] that will be used to resolve the colors used for this thumb in
+     * different states. See [SliderDefaults.colors].
+     * @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.
+     */
+    @Composable
+    fun Thumb(
+        interactionSource: MutableInteractionSource,
+        modifier: Modifier = Modifier,
+        colors: SliderColors = colors(),
+        enabled: Boolean = true,
+        thumbSize: DpSize = ThumbSize
+    ) {
+        val interactions = remember { mutableStateListOf<Interaction>() }
+        LaunchedEffect(interactionSource) {
+            interactionSource.interactions.collect { interaction ->
+                when (interaction) {
+                    is PressInteraction.Press -> interactions.add(interaction)
+                    is PressInteraction.Release -> interactions.remove(interaction.press)
+                    is PressInteraction.Cancel -> interactions.remove(interaction.press)
+                    is DragInteraction.Start -> interactions.add(interaction)
+                    is DragInteraction.Stop -> interactions.remove(interaction.start)
+                    is DragInteraction.Cancel -> interactions.remove(interaction.start)
+                }
             }
+        }
+
+        val elevation = if (interactions.isNotEmpty()) {
+            ThumbPressedElevation
+        } else {
+            ThumbDefaultElevation
+        }
+        val shape = SliderTokens.HandleShape.toShape()
+
+        Spacer(
+            modifier
+                .size(thumbSize)
+                .indication(
+                    interactionSource = interactionSource,
+                    indication = rememberRipple(
+                        bounded = false,
+                        radius = SliderTokens.StateLayerSize / 2
+                    )
+                )
+                .hoverable(interactionSource = interactionSource)
+                .shadow(if (enabled) elevation else 0.dp, shape, clip = false)
+                .background(colors.thumbColor(enabled).value, shape)
+        )
+    }
+
+    /**
+     * The Default track for [Slider] and [RangeSlider]
+     *
+     * @param sliderPositions [SliderPositions] which is used to obtain the current active track
+     * and the tick positions if the slider is discrete.
+     * @param modifier the [Modifier] to be applied to the track.
+     * @param colors [SliderColors] that will be used to resolve the colors used for this track in
+     * different states. See [SliderDefaults.colors].
+     * @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.
+     */
+    @Composable
+    fun Track(
+        sliderPositions: SliderPositions,
+        modifier: Modifier = Modifier,
+        colors: SliderColors = colors(),
+        enabled: Boolean = true,
+    ) {
+        val inactiveTrackColor = colors.trackColor(enabled, active = false)
+        val activeTrackColor = colors.trackColor(enabled, active = true)
+        val inactiveTickColor = colors.tickColor(enabled, active = false)
+        val activeTickColor = colors.tickColor(enabled, active = true)
+        Canvas(modifier
+            .fillMaxWidth()
+            .height(TrackHeight)
+        ) {
+            val isRtl = layoutDirection == LayoutDirection.Rtl
+            val sliderLeft = Offset(0f, center.y)
+            val sliderRight = Offset(size.width, center.y)
+            val sliderStart = if (isRtl) sliderRight else sliderLeft
+            val sliderEnd = if (isRtl) sliderLeft else sliderRight
+            val tickSize = TickSize.toPx()
+            val trackStrokeWidth = TrackHeight.toPx()
+            drawLine(
+                inactiveTrackColor.value,
+                sliderStart,
+                sliderEnd,
+                trackStrokeWidth,
+                StrokeCap.Round
+            )
+            val sliderValueEnd = Offset(
+                sliderStart.x +
+                    (sliderEnd.x - sliderStart.x) * sliderPositions.activeRange.endInclusive,
+                center.y
+            )
+
+            val sliderValueStart = Offset(
+                sliderStart.x +
+                    (sliderEnd.x - sliderStart.x) * sliderPositions.activeRange.start,
+                center.y
+            )
+
+            drawLine(
+                activeTrackColor.value,
+                sliderValueStart,
+                sliderValueEnd,
+                trackStrokeWidth,
+                StrokeCap.Round
+            )
+            sliderPositions.tickFractions.groupBy {
+                it > sliderPositions.activeRange.endInclusive ||
+                    it < sliderPositions.activeRange.start
+            }.forEach { (outsideFraction, list) ->
+                    drawPoints(
+                        list.map {
+                            Offset(lerp(sliderStart, sliderEnd, it).x, center.y)
+                        },
+                        PointMode.Points,
+                        (if (outsideFraction) inactiveTickColor else activeTickColor).value,
+                        tickSize,
+                        StrokeCap.Round
+                    )
+                }
+        }
     }
 }
 
@@ -1185,7 +1414,7 @@
     rawOffsetEnd: State<Float>,
     enabled: Boolean,
     isRtl: Boolean,
-    maxPx: Float,
+    maxPx: Int,
     valueRange: ClosedFloatingPointRange<Float>,
     gestureEndAction: State<(Boolean) -> Unit>,
     onDrag: State<(Boolean, Float) -> Unit>,
@@ -1413,21 +1642,26 @@
     TRACK
 }
 
+private enum class RangeSliderComponents {
+    ENDTHUMB,
+    STARTTHUMB,
+    TRACK
+}
+
 /**
- * Class that holds information about [Slider]'s active track and fractional
- * positions where the discrete ticks should be drawn on the track.
+ * Class that holds information about [Slider]'s and [RangeSlider]'s active track
+ * and fractional positions where the discrete ticks should be drawn on the track.
  */
 @Stable
-@ExperimentalMaterial3Api
 class SliderPositions(
-    initialPositionFraction: Float,
-    initialTickFractions: FloatArray
+    initialActiveRange: ClosedFloatingPointRange<Float> = 0f..1f,
+    initialTickFractions: FloatArray = floatArrayOf()
 ) {
     /**
-     * [Float] within the range [0f, 1f] that indicates the current position of
-     * the thumb as a fraction of the entire track.
+     * [ClosedFloatingPointRange] that indicates the current active range for the
+     * start to thumb for a [Slider] and start thumb to end thumb for a [RangeSlider].
      */
-    var positionFraction: Float by mutableStateOf(initialPositionFraction)
+    var activeRange: ClosedFloatingPointRange<Float> by mutableStateOf(initialActiveRange)
         internal set
 
     /**
@@ -1442,14 +1676,14 @@
         if (this === other) return true
         if (other !is SliderPositions) return false
 
-        if (positionFraction != other.positionFraction) return false
+        if (activeRange != other.activeRange) return false
         if (!tickFractions.contentEquals(other.tickFractions)) return false
 
         return true
     }
 
     override fun hashCode(): Int {
-        var result = positionFraction.hashCode()
+        var result = activeRange.hashCode()
         result = 31 * result + tickFractions.contentHashCode()
         return result
     }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
index 6dca6b4..85baad3 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
@@ -223,7 +223,7 @@
                     content = {
                         Icon(
                             Icons.Filled.Close,
-                            contentDescription = getString(Strings.Dismiss),
+                            contentDescription = getString(Strings.SnackbarDismiss),
                         )
                     }
                 )
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 f65a152..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)
@@ -33,17 +36,46 @@
         val Dialog = Strings(7)
         val MenuExpanded = Strings(8)
         val MenuCollapsed = Strings(9)
-        val Dismiss = Strings(10)
-        val Search = Strings(11)
+        val SnackbarDismiss = Strings(10)
+        val SearchBarSearch = Strings(11)
         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..0d968af
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
@@ -0,0 +1,981 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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 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 contentDescriptionValue = getString(Strings.TimePickerPeriodToggle)
+    Column(
+        Modifier
+            .semantics { contentDescription = contentDescriptionValue }
+            .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 contentDescriptionValue = 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 {
+                role = Role.RadioButton
+                contentDescription = contentDescriptionValue
+            },
+        onClick = {
+            if (selection != state.selection) {
+                state.selection = selection
+                scope.launch {
+                    state.animateToCurrent()
+                }
+            }
+        },
+        selected = selected,
+        shape = TimeSelectorContainerShape.toShape(),
+        color = containerColor,
+    ) {
+        Box(contentAlignment = Alignment.Center) {
+            val textStyle = MaterialTheme.typography.fromToken(TimeSelectorLabelTextFont)
+            Text(
+                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) {
+                    ClockText(
+                        is24Hour = state.is24hour,
+                        selection = state.selection,
+                        value = if (!state.is24hour) screen[it] else screen[it] % 12
+                    )
+                }
+
+                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) {
+                            ClockText(
+                                is24Hour = true,
+                                selection = state.selection,
+                                value = ExtraHours[it]
+                            )
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+@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,
+    selection: Selection,
+    value: Int
+) {
+    val style = MaterialTheme.typography.fromToken(ClockDialLabelTextFont).let {
+        remember(it) {
+            copyAndSetFontPadding(style = it, false)
+        }
+    }
+
+    val description = getString(
+        numberContentDescription(
+            selection = selection,
+            is24Hour = is24Hour
+        ),
+        value
+    )
+
+    Box(
+        contentAlignment = Alignment.Center,
+        modifier = Modifier
+            .minimumInteractiveComponentSize()
+            .focusable()
+            .semantics {
+                contentDescription = description
+            }
+    ) {
+        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 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 776da50..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"
@@ -30,15 +34,44 @@
         Strings.Dialog -> "Dialog"
         Strings.MenuExpanded -> "Expanded"
         Strings.MenuCollapsed -> "Collapsed"
-        Strings.Dismiss -> "Dismiss"
-        Strings.Search -> "Search"
+        Strings.SnackbarDismiss -> "Dismiss"
+        Strings.SearchBarSearch -> "Search"
         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-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 992384a..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 = 9200
+    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/OWNERS b/compose/ui/ui-test-junit4/OWNERS
index 90ad461..0b4fcdd 100644
--- a/compose/ui/ui-test-junit4/OWNERS
+++ b/compose/ui/ui-test-junit4/OWNERS
@@ -1,2 +1,3 @@
 # Bug component: 741702
 jellefresen@google.com
+klippenstein@google.com
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/OWNERS b/compose/ui/ui-test/OWNERS
index 90ad461..0b4fcdd 100644
--- a/compose/ui/ui-test/OWNERS
+++ b/compose/ui/ui-test/OWNERS
@@ -1,2 +1,3 @@
 # Bug component: 741702
 jellefresen@google.com
+klippenstein@google.com
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.txt b/compose/ui/ui-text/api/current.txt
index ed7c220..d2dd899 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -22,6 +22,7 @@
     method public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<java.lang.String>> getStringAnnotations(int start, int end);
     method public String getText();
     method public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.TtsAnnotation>> getTtsAnnotations(int start, int end);
+    method public boolean hasStringAnnotations(String tag, int start, int end);
     method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.AnnotatedString plus(androidx.compose.ui.text.AnnotatedString other);
     method public androidx.compose.ui.text.AnnotatedString subSequence(int startIndex, int endIndex);
     method public androidx.compose.ui.text.AnnotatedString subSequence(long range);
@@ -237,10 +238,14 @@
   }
 
   @androidx.compose.runtime.Immutable public final class ParagraphStyle {
-    ctor public ParagraphStyle(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 public ParagraphStyle(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.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
-    method public androidx.compose.ui.text.ParagraphStyle copy(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 public androidx.compose.ui.text.ParagraphStyle copy(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.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    ctor public ParagraphStyle(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.PlatformParagraphStyle? 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 ParagraphStyle(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 ParagraphStyle(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.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    method public androidx.compose.ui.text.ParagraphStyle copy(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.PlatformParagraphStyle? 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.ParagraphStyle copy(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.ParagraphStyle copy(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.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    method public androidx.compose.ui.text.style.Hyphens? getHyphens();
+    method public androidx.compose.ui.text.style.LineBreak? getLineBreak();
     method public long getLineHeight();
     method public androidx.compose.ui.text.style.LineHeightStyle? getLineHeightStyle();
     method public androidx.compose.ui.text.PlatformParagraphStyle? getPlatformStyle();
@@ -249,6 +254,8 @@
     method public androidx.compose.ui.text.style.TextIndent? getTextIndent();
     method @androidx.compose.runtime.Stable public androidx.compose.ui.text.ParagraphStyle merge(optional androidx.compose.ui.text.ParagraphStyle? other);
     method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.ParagraphStyle plus(androidx.compose.ui.text.ParagraphStyle other);
+    property public final androidx.compose.ui.text.style.Hyphens? hyphens;
+    property public final androidx.compose.ui.text.style.LineBreak? lineBreak;
     property public final long lineHeight;
     property public final androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle;
     property public final androidx.compose.ui.text.PlatformParagraphStyle? platformStyle;
@@ -300,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;
@@ -506,10 +512,12 @@
   }
 
   @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);
-    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);
-    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);
-    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);
+    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);
+    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 public long getBackground();
     method public androidx.compose.ui.text.style.BaselineShift? getBaselineShift();
     method public long getColor();
@@ -519,7 +527,9 @@
     method public androidx.compose.ui.text.font.FontStyle? getFontStyle();
     method public androidx.compose.ui.text.font.FontSynthesis? getFontSynthesis();
     method public androidx.compose.ui.text.font.FontWeight? getFontWeight();
+    method public androidx.compose.ui.text.style.Hyphens? getHyphens();
     method public long getLetterSpacing();
+    method public androidx.compose.ui.text.style.LineBreak? getLineBreak();
     method public long getLineHeight();
     method public androidx.compose.ui.text.style.LineHeightStyle? getLineHeightStyle();
     method public androidx.compose.ui.text.intl.LocaleList? getLocaleList();
@@ -548,7 +558,9 @@
     property public final androidx.compose.ui.text.font.FontStyle? fontStyle;
     property public final androidx.compose.ui.text.font.FontSynthesis? fontSynthesis;
     property public final androidx.compose.ui.text.font.FontWeight? fontWeight;
+    property public final androidx.compose.ui.text.style.Hyphens? hyphens;
     property public final long letterSpacing;
+    property public final androidx.compose.ui.text.style.LineBreak? lineBreak;
     property public final long lineHeight;
     property public final androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle;
     property public final androidx.compose.ui.text.intl.LocaleList? localeList;
@@ -698,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 {
@@ -1345,6 +1357,77 @@
     method @androidx.compose.runtime.Stable public static float lerp(float start, float stop, float fraction);
   }
 
+  @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 int getAuto();
+    method public int getNone();
+    property public final int Auto;
+    property public final int None;
+  }
+
+  @androidx.compose.runtime.Immutable public final class LineBreak {
+    ctor public LineBreak(int strategy, int strictness, int wordBreak);
+    method public androidx.compose.ui.text.style.LineBreak copy(optional int strategy, optional int strictness, optional int wordBreak);
+    method public int getStrategy();
+    method public int getStrictness();
+    method public int getWordBreak();
+    property public final int strategy;
+    property public final int strictness;
+    property public final int wordBreak;
+    field public static final androidx.compose.ui.text.style.LineBreak.Companion Companion;
+  }
+
+  public static final class LineBreak.Companion {
+    method public androidx.compose.ui.text.style.LineBreak getHeading();
+    method public androidx.compose.ui.text.style.LineBreak getParagraph();
+    method public androidx.compose.ui.text.style.LineBreak getSimple();
+    property public final androidx.compose.ui.text.style.LineBreak Heading;
+    property public final androidx.compose.ui.text.style.LineBreak Paragraph;
+    property public final androidx.compose.ui.text.style.LineBreak Simple;
+  }
+
+  @kotlin.jvm.JvmInline public static final value class LineBreak.Strategy {
+    field public static final androidx.compose.ui.text.style.LineBreak.Strategy.Companion Companion;
+  }
+
+  public static final class LineBreak.Strategy.Companion {
+    method public int getBalanced();
+    method public int getHighQuality();
+    method public int getSimple();
+    property public final int Balanced;
+    property public final int HighQuality;
+    property public final int Simple;
+  }
+
+  @kotlin.jvm.JvmInline public static final value class LineBreak.Strictness {
+    field public static final androidx.compose.ui.text.style.LineBreak.Strictness.Companion Companion;
+  }
+
+  public static final class LineBreak.Strictness.Companion {
+    method public int getDefault();
+    method public int getLoose();
+    method public int getNormal();
+    method public int getStrict();
+    property public final int Default;
+    property public final int Loose;
+    property public final int Normal;
+    property public final int Strict;
+  }
+
+  @kotlin.jvm.JvmInline public static final value class LineBreak.WordBreak {
+    field public static final androidx.compose.ui.text.style.LineBreak.WordBreak.Companion Companion;
+  }
+
+  public static final class LineBreak.WordBreak.Companion {
+    method public int getDefault();
+    method public int getPhrase();
+    property public final int Default;
+    property public final int Phrase;
+  }
+
   public final class LineHeightStyle {
     ctor public LineHeightStyle(float alignment, int trim);
     method public float getAlignment();
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 7db6336..c3c178e 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -23,6 +23,7 @@
     method public String getText();
     method public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.TtsAnnotation>> getTtsAnnotations(int start, int end);
     method @androidx.compose.ui.text.ExperimentalTextApi public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.UrlAnnotation>> getUrlAnnotations(int start, int end);
+    method public boolean hasStringAnnotations(String tag, int start, int end);
     method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.AnnotatedString plus(androidx.compose.ui.text.AnnotatedString other);
     method public androidx.compose.ui.text.AnnotatedString subSequence(int startIndex, int endIndex);
     method public androidx.compose.ui.text.AnnotatedString subSequence(long range);
@@ -255,13 +256,15 @@
 
   @androidx.compose.runtime.Immutable public final class ParagraphStyle {
     ctor @androidx.compose.ui.text.ExperimentalTextApi public ParagraphStyle(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.PlatformParagraphStyle? 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 public ParagraphStyle(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 public ParagraphStyle(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.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
-    method public androidx.compose.ui.text.ParagraphStyle copy(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 public androidx.compose.ui.text.ParagraphStyle copy(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.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    ctor public ParagraphStyle(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.PlatformParagraphStyle? 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 ParagraphStyle(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 ParagraphStyle(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.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    method public androidx.compose.ui.text.ParagraphStyle copy(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.PlatformParagraphStyle? 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.ParagraphStyle copy(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.PlatformParagraphStyle? 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.style.Hyphens? getHyphens();
-    method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.style.LineBreak? getLineBreak();
+    method @Deprecated public androidx.compose.ui.text.ParagraphStyle copy(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.ParagraphStyle copy(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.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    method public androidx.compose.ui.text.style.Hyphens? getHyphens();
+    method public androidx.compose.ui.text.style.LineBreak? getLineBreak();
     method public long getLineHeight();
     method public androidx.compose.ui.text.style.LineHeightStyle? getLineHeightStyle();
     method public androidx.compose.ui.text.PlatformParagraphStyle? getPlatformStyle();
@@ -271,8 +274,8 @@
     method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.style.TextMotion? getTextMotion();
     method @androidx.compose.runtime.Stable public androidx.compose.ui.text.ParagraphStyle merge(optional androidx.compose.ui.text.ParagraphStyle? other);
     method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.ParagraphStyle plus(androidx.compose.ui.text.ParagraphStyle other);
-    property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.Hyphens? hyphens;
-    property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.LineBreak? lineBreak;
+    property public final androidx.compose.ui.text.style.Hyphens? hyphens;
+    property public final androidx.compose.ui.text.style.LineBreak? lineBreak;
     property public final long lineHeight;
     property public final androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle;
     property public final androidx.compose.ui.text.PlatformParagraphStyle? platformStyle;
@@ -325,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;
@@ -500,6 +502,7 @@
   @androidx.compose.runtime.Immutable @androidx.compose.ui.text.ExperimentalTextApi public final class TextMeasurer {
     ctor public TextMeasurer(androidx.compose.ui.text.font.FontFamily.Resolver fallbackFontFamilyResolver, androidx.compose.ui.unit.Density fallbackDensity, androidx.compose.ui.unit.LayoutDirection fallbackLayoutDirection, optional int cacheSize);
     method @androidx.compose.runtime.Stable public androidx.compose.ui.text.TextLayoutResult measure(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.text.TextStyle style, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.Placeholder>> placeholders, optional long constraints, optional androidx.compose.ui.unit.LayoutDirection layoutDirection, optional androidx.compose.ui.unit.Density density, optional androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional boolean skipCache);
+    method @androidx.compose.runtime.Stable public androidx.compose.ui.text.TextLayoutResult measure(String text, optional androidx.compose.ui.text.TextStyle style, optional int overflow, optional boolean softWrap, optional int maxLines, optional long constraints, optional androidx.compose.ui.unit.LayoutDirection layoutDirection, optional androidx.compose.ui.unit.Density density, optional androidx.compose.ui.text.font.FontFamily.Resolver fontFamilyResolver, optional boolean skipCache);
   }
 
   public final class TextMeasurerKt {
@@ -550,14 +553,16 @@
   }
 
   @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);
-    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);
+    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);
-    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);
-    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);
+    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 @androidx.compose.ui.text.ExperimentalTextApi public float getAlpha();
     method public long getBackground();
     method public androidx.compose.ui.text.style.BaselineShift? getBaselineShift();
@@ -570,9 +575,9 @@
     method public androidx.compose.ui.text.font.FontStyle? getFontStyle();
     method public androidx.compose.ui.text.font.FontSynthesis? getFontSynthesis();
     method public androidx.compose.ui.text.font.FontWeight? getFontWeight();
-    method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.style.Hyphens? getHyphens();
+    method public androidx.compose.ui.text.style.Hyphens? getHyphens();
     method public long getLetterSpacing();
-    method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.style.LineBreak? getLineBreak();
+    method public androidx.compose.ui.text.style.LineBreak? getLineBreak();
     method public long getLineHeight();
     method public androidx.compose.ui.text.style.LineHeightStyle? getLineHeightStyle();
     method public androidx.compose.ui.text.intl.LocaleList? getLocaleList();
@@ -605,9 +610,9 @@
     property public final androidx.compose.ui.text.font.FontStyle? fontStyle;
     property public final androidx.compose.ui.text.font.FontSynthesis? fontSynthesis;
     property public final androidx.compose.ui.text.font.FontWeight? fontWeight;
-    property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.Hyphens? hyphens;
+    property public final androidx.compose.ui.text.style.Hyphens? hyphens;
     property public final long letterSpacing;
-    property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.LineBreak? lineBreak;
+    property public final androidx.compose.ui.text.style.LineBreak? lineBreak;
     property public final long lineHeight;
     property public final androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle;
     property public final androidx.compose.ui.text.intl.LocaleList? localeList;
@@ -767,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 {
@@ -1420,18 +1425,18 @@
     method @androidx.compose.runtime.Stable public static float lerp(float start, float stop, float fraction);
   }
 
-  @androidx.compose.ui.text.ExperimentalTextApi 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 @androidx.compose.ui.text.ExperimentalTextApi public final class LineBreak {
+  @androidx.compose.runtime.Immutable public final class LineBreak {
     ctor public LineBreak(int strategy, int strictness, int wordBreak);
     method public androidx.compose.ui.text.style.LineBreak copy(optional int strategy, optional int strictness, optional int wordBreak);
     method public int getStrategy();
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index ed7c220..d2dd899 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -22,6 +22,7 @@
     method public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<java.lang.String>> getStringAnnotations(int start, int end);
     method public String getText();
     method public java.util.List<androidx.compose.ui.text.AnnotatedString.Range<androidx.compose.ui.text.TtsAnnotation>> getTtsAnnotations(int start, int end);
+    method public boolean hasStringAnnotations(String tag, int start, int end);
     method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.AnnotatedString plus(androidx.compose.ui.text.AnnotatedString other);
     method public androidx.compose.ui.text.AnnotatedString subSequence(int startIndex, int endIndex);
     method public androidx.compose.ui.text.AnnotatedString subSequence(long range);
@@ -237,10 +238,14 @@
   }
 
   @androidx.compose.runtime.Immutable public final class ParagraphStyle {
-    ctor public ParagraphStyle(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 public ParagraphStyle(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.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
-    method public androidx.compose.ui.text.ParagraphStyle copy(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 public androidx.compose.ui.text.ParagraphStyle copy(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.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    ctor public ParagraphStyle(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.PlatformParagraphStyle? 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 ParagraphStyle(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 ParagraphStyle(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.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    method public androidx.compose.ui.text.ParagraphStyle copy(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.PlatformParagraphStyle? 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.ParagraphStyle copy(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.ParagraphStyle copy(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.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    method public androidx.compose.ui.text.style.Hyphens? getHyphens();
+    method public androidx.compose.ui.text.style.LineBreak? getLineBreak();
     method public long getLineHeight();
     method public androidx.compose.ui.text.style.LineHeightStyle? getLineHeightStyle();
     method public androidx.compose.ui.text.PlatformParagraphStyle? getPlatformStyle();
@@ -249,6 +254,8 @@
     method public androidx.compose.ui.text.style.TextIndent? getTextIndent();
     method @androidx.compose.runtime.Stable public androidx.compose.ui.text.ParagraphStyle merge(optional androidx.compose.ui.text.ParagraphStyle? other);
     method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.ParagraphStyle plus(androidx.compose.ui.text.ParagraphStyle other);
+    property public final androidx.compose.ui.text.style.Hyphens? hyphens;
+    property public final androidx.compose.ui.text.style.LineBreak? lineBreak;
     property public final long lineHeight;
     property public final androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle;
     property public final androidx.compose.ui.text.PlatformParagraphStyle? platformStyle;
@@ -300,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;
@@ -506,10 +512,12 @@
   }
 
   @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);
-    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);
-    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);
-    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);
+    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);
+    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 public long getBackground();
     method public androidx.compose.ui.text.style.BaselineShift? getBaselineShift();
     method public long getColor();
@@ -519,7 +527,9 @@
     method public androidx.compose.ui.text.font.FontStyle? getFontStyle();
     method public androidx.compose.ui.text.font.FontSynthesis? getFontSynthesis();
     method public androidx.compose.ui.text.font.FontWeight? getFontWeight();
+    method public androidx.compose.ui.text.style.Hyphens? getHyphens();
     method public long getLetterSpacing();
+    method public androidx.compose.ui.text.style.LineBreak? getLineBreak();
     method public long getLineHeight();
     method public androidx.compose.ui.text.style.LineHeightStyle? getLineHeightStyle();
     method public androidx.compose.ui.text.intl.LocaleList? getLocaleList();
@@ -548,7 +558,9 @@
     property public final androidx.compose.ui.text.font.FontStyle? fontStyle;
     property public final androidx.compose.ui.text.font.FontSynthesis? fontSynthesis;
     property public final androidx.compose.ui.text.font.FontWeight? fontWeight;
+    property public final androidx.compose.ui.text.style.Hyphens? hyphens;
     property public final long letterSpacing;
+    property public final androidx.compose.ui.text.style.LineBreak? lineBreak;
     property public final long lineHeight;
     property public final androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle;
     property public final androidx.compose.ui.text.intl.LocaleList? localeList;
@@ -698,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 {
@@ -1345,6 +1357,77 @@
     method @androidx.compose.runtime.Stable public static float lerp(float start, float stop, float fraction);
   }
 
+  @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 int getAuto();
+    method public int getNone();
+    property public final int Auto;
+    property public final int None;
+  }
+
+  @androidx.compose.runtime.Immutable public final class LineBreak {
+    ctor public LineBreak(int strategy, int strictness, int wordBreak);
+    method public androidx.compose.ui.text.style.LineBreak copy(optional int strategy, optional int strictness, optional int wordBreak);
+    method public int getStrategy();
+    method public int getStrictness();
+    method public int getWordBreak();
+    property public final int strategy;
+    property public final int strictness;
+    property public final int wordBreak;
+    field public static final androidx.compose.ui.text.style.LineBreak.Companion Companion;
+  }
+
+  public static final class LineBreak.Companion {
+    method public androidx.compose.ui.text.style.LineBreak getHeading();
+    method public androidx.compose.ui.text.style.LineBreak getParagraph();
+    method public androidx.compose.ui.text.style.LineBreak getSimple();
+    property public final androidx.compose.ui.text.style.LineBreak Heading;
+    property public final androidx.compose.ui.text.style.LineBreak Paragraph;
+    property public final androidx.compose.ui.text.style.LineBreak Simple;
+  }
+
+  @kotlin.jvm.JvmInline public static final value class LineBreak.Strategy {
+    field public static final androidx.compose.ui.text.style.LineBreak.Strategy.Companion Companion;
+  }
+
+  public static final class LineBreak.Strategy.Companion {
+    method public int getBalanced();
+    method public int getHighQuality();
+    method public int getSimple();
+    property public final int Balanced;
+    property public final int HighQuality;
+    property public final int Simple;
+  }
+
+  @kotlin.jvm.JvmInline public static final value class LineBreak.Strictness {
+    field public static final androidx.compose.ui.text.style.LineBreak.Strictness.Companion Companion;
+  }
+
+  public static final class LineBreak.Strictness.Companion {
+    method public int getDefault();
+    method public int getLoose();
+    method public int getNormal();
+    method public int getStrict();
+    property public final int Default;
+    property public final int Loose;
+    property public final int Normal;
+    property public final int Strict;
+  }
+
+  @kotlin.jvm.JvmInline public static final value class LineBreak.WordBreak {
+    field public static final androidx.compose.ui.text.style.LineBreak.WordBreak.Companion Companion;
+  }
+
+  public static final class LineBreak.WordBreak.Companion {
+    method public int getDefault();
+    method public int getPhrase();
+    property public final int Default;
+    property public final int Phrase;
+  }
+
   public final class LineHeightStyle {
     ctor public LineHeightStyle(float alignment, int trim);
     method public float getAlignment();
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/samples/src/main/java/androidx/compose/ui/text/samples/LineBreakSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/LineBreakSamples.kt
index 91cf6d9..ed6b96a 100644
--- a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/LineBreakSamples.kt
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/LineBreakSamples.kt
@@ -19,12 +19,10 @@
 import androidx.annotation.Sampled
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.style.LineBreak
 import androidx.compose.ui.unit.sp
 
-@OptIn(ExperimentalTextApi::class)
 @Sampled
 @Composable
 fun LineBreakSample() {
@@ -44,7 +42,6 @@
     )
 }
 
-@OptIn(ExperimentalTextApi::class)
 @Sampled
 @Composable
 fun AndroidLineBreakSample() {
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/TextMeasurerTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
index f98d0dd..37667e3 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
@@ -24,11 +24,14 @@
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.createFontFamilyResolver
 import androidx.compose.ui.text.font.toFontFamily
+import androidx.compose.ui.text.matchers.assertThat
 import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextDirection
 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
+import androidx.compose.ui.unit.sp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
@@ -55,6 +58,16 @@
     private val multiLineText = AnnotatedString("Lorem\nipsum\ndolor\nsit\namet")
 
     @Test
+    fun stringAndAnnotatedString_shouldReturnTheSameInstance_whenCacheIsEnabled() {
+        val textMeasurer = textMeasurer(cacheSize = 8)
+        val textLayoutResult1 = textMeasurer.measure("Hello")
+        val textLayoutResult2 = textMeasurer.measure(AnnotatedString("Hello"))
+
+        assertThat(textLayoutResult1.multiParagraph)
+            .isSameInstanceAs(textLayoutResult2.multiParagraph)
+    }
+
+    @Test
     fun width_shouldMatter_ifSoftwrapIsEnabled() {
         val textLayoutResult = layoutText(
             textLayoutInput(
@@ -170,6 +183,82 @@
     }
 
     @Test
+    fun textLayout_cannotBeSmallerThan_minWidth() {
+        val textLayoutResult = layoutText(
+            textLayoutInput(
+                text = AnnotatedString("A"),
+                softWrap = false,
+                overflow = TextOverflow.Clip,
+                constraints = Constraints.fixedWidth(400)
+            )
+        )
+
+        assertThat(textLayoutResult.size.width).isEqualTo(400)
+    }
+
+    @Test
+    fun textLayout_canBeSmallerThan_maxWidth() {
+        val fontSize = 10
+        val textLayoutResult = layoutText(
+            textLayoutInput(
+                text = AnnotatedString("A"),
+                style = TextStyle(fontSize = fontSize.sp),
+                softWrap = false,
+                overflow = TextOverflow.Clip,
+                constraints = Constraints(maxWidth = 400)
+            )
+        )
+
+        assertThat(textLayoutResult.size.width).isEqualTo(fontSize)
+    }
+
+    @Test
+    fun textLayout_cannotBeSmallerThan_minHeight() {
+        val textLayoutResult = layoutText(
+            textLayoutInput(
+                text = AnnotatedString("A"),
+                softWrap = false,
+                overflow = TextOverflow.Clip,
+                constraints = Constraints.fixedHeight(400)
+            )
+        )
+
+        assertThat(textLayoutResult.size.height).isEqualTo(400)
+    }
+
+    @Test
+    fun textLayout_canBeSmallerThan_maxHeight() {
+        val fontSize = 10.sp
+        val textLayoutResult = layoutText(
+            textLayoutInput(
+                text = AnnotatedString("A"),
+                style = TextStyle(fontSize = fontSize),
+                softWrap = false,
+                overflow = TextOverflow.Clip,
+                constraints = Constraints(maxHeight = 400)
+            )
+        )
+
+        assertThat(textLayoutResult.size.height).isEqualTo(10)
+    }
+
+    @Test
+    fun layoutDirection_shouldDictate_textDirection() {
+        val textLayoutResult1 = layoutText(textLayoutInput(
+            text = multiLineText,
+            layoutDirection = LayoutDirection.Rtl
+        ))
+
+        val textLayoutResult2 = layoutText(textLayoutInput(
+            text = multiLineText,
+            style = TextStyle(textDirection = TextDirection.Rtl)
+        ))
+
+        assertThat(textLayoutResult1.multiParagraph.bitmap())
+            .isEqualToBitmap(textLayoutResult2.multiParagraph.bitmap())
+    }
+
+    @Test
     fun colorShouldChangeInResult_whenCacheIsActive() {
         val textMeasurer = textMeasurer(cacheSize = 8)
         val firstTextLayout = layoutText(
@@ -321,7 +410,8 @@
 
     private fun layoutText(
         textLayoutInput: TextLayoutInput,
-        textMeasurer: TextMeasurer? = null
+        textMeasurer: TextMeasurer? = null,
+        skipCache: Boolean = false
     ) = with(textLayoutInput) {
         (textMeasurer ?: textMeasurer()).measure(
             text = text,
@@ -331,6 +421,10 @@
             maxLines = maxLines,
             placeholders = placeholders,
             constraints = constraints,
+            layoutDirection = layoutDirection,
+            density = density,
+            fontFamilyResolver = fontFamilyResolver,
+            skipCache = skipCache
         )
     }
 }
\ No newline at end of file
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/androidMain/kotlin/androidx/compose/ui/text/style/LineBreak.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/style/LineBreak.android.kt
index b98a775..e5d7061 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/style/LineBreak.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/style/LineBreak.android.kt
@@ -17,7 +17,9 @@
 package androidx.compose.ui.text.style
 
 import androidx.compose.runtime.Immutable
-import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.style.LineBreak.Strategy
+import androidx.compose.ui.text.style.LineBreak.Strictness
+import androidx.compose.ui.text.style.LineBreak.WordBreak
 
 // TODO(b/246340708): Remove @sample LineBreakSample from the actual class
 /**
@@ -40,7 +42,6 @@
  * @param strictness defines the line breaking rules
  * @param wordBreak defines how words are broken
  */
-@ExperimentalTextApi
 @Immutable
 actual class LineBreak(
     val strategy: Strategy,
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 5deccde..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
@@ -21,6 +21,7 @@
 import androidx.compose.ui.text.AnnotatedString.Builder
 import androidx.compose.ui.text.AnnotatedString.Range
 import androidx.compose.ui.text.intl.LocaleList
+import androidx.compose.ui.util.fastAny
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastMap
 
@@ -31,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].
@@ -59,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"
             }
@@ -95,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)
         )
     }
@@ -136,9 +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)
+        } ?: false
 
     /**
      * Query all of the string annotations attached on this AnnotatedString.
@@ -151,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].
@@ -166,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].
@@ -182,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
     }
 
@@ -365,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)
                 )
@@ -392,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,
@@ -608,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 }
             )
         }
     }
@@ -635,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>>()
@@ -668,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
@@ -694,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
@@ -720,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
@@ -752,7 +789,7 @@
 ): AnnotatedString {
     return AnnotatedString(
         text = if (start != end) text.substring(start, end) else "",
-        spanStyles = getLocalSpanStyles(start, end)
+        spanStylesOrNull = getLocalSpanStyles(start, end)
     )
 }
 
@@ -1005,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/ParagraphStyle.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt
index 6c108cc3..f84ba8f 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt
@@ -69,13 +69,7 @@
     val textIndent: TextIndent? = null,
     val platformStyle: PlatformParagraphStyle? = null,
     val lineHeightStyle: LineHeightStyle? = null,
-    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
-    @get:ExperimentalTextApi
-    @property:ExperimentalTextApi
     val lineBreak: LineBreak? = null,
-    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
-    @get:ExperimentalTextApi
-    @property:ExperimentalTextApi
     val hyphens: Hyphens? = null,
     @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
     @get:ExperimentalTextApi
@@ -83,27 +77,12 @@
     val textMotion: TextMotion? = null
 ) {
 
-    /**
-     * Paragraph styling configuration for a paragraph. The difference between [SpanStyle] and
-     * `ParagraphStyle` is that, `ParagraphStyle` can be applied to a whole [Paragraph] while
-     * [SpanStyle] can be applied at the character level.
-     * Once a portion of the text is marked with a `ParagraphStyle`, that portion will be separated from
-     * the remaining as if a line feed character was added.
-     *
-     * @sample androidx.compose.ui.text.samples.ParagraphStyleSample
-     * @sample androidx.compose.ui.text.samples.ParagraphStyleAnnotatedStringsSample
-     *
-     * @param textAlign The alignment of the text within the lines of the paragraph.
-     * @param textDirection The algorithm to be used to resolve the final text direction:
-     * Left To Right or Right To Left.
-     * @param lineHeight Line height for the [Paragraph] in [TextUnit] unit, e.g. SP or EM.
-     * @param textIndent The indentation of the paragraph.
-     *
-     * @see Paragraph
-     * @see AnnotatedString
-     * @see SpanStyle
-     * @see TextStyle
-     */
+    @Deprecated(
+        "ParagraphStyle constructors that do not take new stable parameters " +
+            "like LineHeightStyle, LineBreak, Hyphens are deprecated. Please use the new stable " +
+            "constructor.",
+        level = DeprecationLevel.HIDDEN
+    )
     @OptIn(ExperimentalTextApi::class)
     constructor(
         textAlign: TextAlign? = null,
@@ -122,6 +101,32 @@
         textMotion = null
     )
 
+    @Deprecated(
+        "ParagraphStyle constructors that do not take new stable parameters " +
+            "like LineHeightStyle, LineBreak, Hyphens are deprecated. Please use the new stable " +
+            "constructors.",
+        level = DeprecationLevel.HIDDEN
+    )
+    @OptIn(ExperimentalTextApi::class)
+    constructor(
+        textAlign: TextAlign? = null,
+        textDirection: TextDirection? = null,
+        lineHeight: TextUnit = TextUnit.Unspecified,
+        textIndent: TextIndent? = null,
+        platformStyle: PlatformParagraphStyle? = null,
+        lineHeightStyle: LineHeightStyle? = null
+    ) : this(
+        textAlign = textAlign,
+        textDirection = textDirection,
+        lineHeight = lineHeight,
+        textIndent = textIndent,
+        platformStyle = platformStyle,
+        lineHeightStyle = lineHeightStyle,
+        lineBreak = null,
+        hyphens = null,
+        textMotion = null
+    )
+
     /**
      * Paragraph styling configuration for a paragraph. The difference between [SpanStyle] and
      * `ParagraphStyle` is that, `ParagraphStyle` can be applied to a whole [Paragraph] while
@@ -142,13 +147,14 @@
      * line, whether to apply additional space as a result of line height to top of first line top and
      * bottom of last line. The configuration is applied only when a [lineHeight] is defined.
      * When null, [LineHeightStyle.Default] is used.
+     * @param lineBreak The line breaking configuration for the text.
+     * @param hyphens The configuration of hyphenation.
      *
      * @see Paragraph
      * @see AnnotatedString
      * @see SpanStyle
      * @see TextStyle
      */
-    // TODO(b/245939557, b/246715337): Deprecate this when LineBreak and Hyphens are stable
     @OptIn(ExperimentalTextApi::class)
     constructor(
         textAlign: TextAlign? = null,
@@ -156,7 +162,9 @@
         lineHeight: TextUnit = TextUnit.Unspecified,
         textIndent: TextIndent? = null,
         platformStyle: PlatformParagraphStyle? = null,
-        lineHeightStyle: LineHeightStyle? = null
+        lineHeightStyle: LineHeightStyle? = null,
+        lineBreak: LineBreak? = null,
+        hyphens: Hyphens? = null
     ) : this(
         textAlign = textAlign,
         textDirection = textDirection,
@@ -164,8 +172,9 @@
         textIndent = textIndent,
         platformStyle = platformStyle,
         lineHeightStyle = lineHeightStyle,
-        lineBreak = null,
-        hyphens = null
+        lineBreak = lineBreak,
+        hyphens = hyphens,
+        textMotion = null
     )
 
     init {
@@ -217,6 +226,12 @@
     @Stable
     operator fun plus(other: ParagraphStyle): ParagraphStyle = this.merge(other)
 
+    @Deprecated(
+        "ParagraphStyle copy constructors that do not take new stable parameters " +
+            "like LineHeightStyle, LineBreak, Hyphens are deprecated. Please use the new stable " +
+            "copy constructor.",
+        level = DeprecationLevel.HIDDEN
+    )
     @OptIn(ExperimentalTextApi::class)
     fun copy(
         textAlign: TextAlign? = this.textAlign,
@@ -237,7 +252,12 @@
         )
     }
 
-    // TODO(b/246715337, b/245939557): Deprecate this when Hyphens and LineBreak are stable
+    @Deprecated(
+        "ParagraphStyle copy constructors that do not take new stable parameters " +
+            "like LineHeightStyle, LineBreak, Hyphens are deprecated. Please use the new stable " +
+            "copy constructor.",
+        level = DeprecationLevel.HIDDEN
+    )
     @OptIn(ExperimentalTextApi::class)
     fun copy(
         textAlign: TextAlign? = this.textAlign,
@@ -260,6 +280,30 @@
         )
     }
 
+    @OptIn(ExperimentalTextApi::class)
+    fun copy(
+        textAlign: TextAlign? = this.textAlign,
+        textDirection: TextDirection? = this.textDirection,
+        lineHeight: TextUnit = this.lineHeight,
+        textIndent: TextIndent? = this.textIndent,
+        platformStyle: PlatformParagraphStyle? = this.platformStyle,
+        lineHeightStyle: LineHeightStyle? = this.lineHeightStyle,
+        lineBreak: LineBreak? = this.lineBreak,
+        hyphens: Hyphens? = this.hyphens
+    ): ParagraphStyle {
+        return ParagraphStyle(
+            textAlign = textAlign,
+            textDirection = textDirection,
+            lineHeight = lineHeight,
+            textIndent = textIndent,
+            platformStyle = platformStyle,
+            lineHeightStyle = lineHeightStyle,
+            lineBreak = lineBreak,
+            hyphens = hyphens,
+            textMotion = this.textMotion
+        )
+    }
+
     @ExperimentalTextApi
     fun copy(
         textAlign: TextAlign? = this.textAlign,
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/TextMeasurer.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
index 873d665..9ca36a4 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
@@ -183,6 +183,73 @@
         }
     }
 
+    /**
+     * Creates a [TextLayoutResult] according to given parameters.
+     *
+     * This function supports laying out text that consists of multiple paragraphs, includes
+     * placeholders, wraps around soft line breaks, and might overflow outside the specified size.
+     *
+     * Most parameters for text affect the final text layout. One pixel change in [constraints]
+     * boundaries can displace a word to another line which would cause a chain reaction that
+     * completely changes how text is rendered.
+     *
+     * On the other hand, some attributes only play a role when drawing the created text layout.
+     * For example text layout can be created completely in black color but we can apply
+     * [TextStyle.color] later in draw phase. This also means that animating text color shouldn't
+     * invalidate text layout.
+     *
+     * Thus, [textLayoutCache] helps in the process of converting a set of text layout inputs to
+     * a text layout while ignoring non-layout-affecting attributes. Iterative calls that use the
+     * same input parameters should benefit from substantial performance improvements.
+     *
+     * @param text the text to be laid out
+     * @param style the [TextStyle] to be applied to the whole text
+     * @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 constraints how wide and tall the text is allowed to be. [Constraints.maxWidth]
+     * will define the width of the MultiParagraph. [Constraints.maxHeight] helps defining the
+     * number of lines that fit with ellipsis is true. [Constraints.minWidth] defines the minimum
+     * width the resulting [TextLayoutResult.size] will report. [Constraints.minHeight] is no-op.
+     * @param layoutDirection layout direction of the measurement environment. If not specified,
+     * defaults to the value that was given during initialization of this [TextMeasurer].
+     * @param density density of the measurement environment. If not specified, defaults to
+     * the value that was given during initialization of this [TextMeasurer].
+     * @param fontFamilyResolver to be used to load the font given in [SpanStyle]s. If not
+     * specified, defaults to the value that was given during initialization of this [TextMeasurer].
+     * @param skipCache Disables cache optimization if it is passed as true.
+     */
+    @Stable
+    fun measure(
+        text: String,
+        style: TextStyle = TextStyle.Default,
+        overflow: TextOverflow = TextOverflow.Clip,
+        softWrap: Boolean = true,
+        maxLines: Int = Int.MAX_VALUE,
+        constraints: Constraints = Constraints(),
+        layoutDirection: LayoutDirection = this.fallbackLayoutDirection,
+        density: Density = this.fallbackDensity,
+        fontFamilyResolver: FontFamily.Resolver = this.fallbackFontFamilyResolver,
+        skipCache: Boolean = false
+    ): TextLayoutResult {
+        return measure(
+            text = AnnotatedString(text),
+            style = style,
+            overflow = overflow,
+            softWrap = softWrap,
+            maxLines = maxLines,
+            constraints = constraints,
+            layoutDirection = layoutDirection,
+            density = density,
+            fontFamilyResolver = fontFamilyResolver,
+            skipCache = skipCache
+        )
+    }
+
     internal companion object {
         /**
          * Computes the visual position of the glyphs for painting the text.
@@ -203,8 +270,7 @@
             )
 
             val minWidth = constraints.minWidth
-            val widthMatters = softWrap ||
-                overflow == TextOverflow.Ellipsis
+            val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
             val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
                 constraints.maxWidth
             } else {
@@ -225,8 +291,7 @@
             //     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 overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
             val finalMaxLines = if (overwriteMaxLines) 1 else maxLines
 
             // if minWidth == maxWidth the width is fixed.
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt
index 25a5551..fbcae0a 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextPainter.kt
@@ -27,10 +27,12 @@
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.graphics.drawscope.DrawStyle
 import androidx.compose.ui.graphics.drawscope.DrawTransform
+import androidx.compose.ui.graphics.drawscope.Fill
 import androidx.compose.ui.graphics.drawscope.withTransform
 import androidx.compose.ui.graphics.isUnspecified
 import androidx.compose.ui.graphics.takeOrElse
 import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextForegroundStyle.Unspecified
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.modulate
 import androidx.compose.ui.unit.Constraints
@@ -57,27 +59,40 @@
             canvas.save()
             canvas.clipRect(bounds)
         }
-        val resolvedSpanStyle = resolveSpanStyleDefaults(
-            textLayoutResult.layoutInput.style.spanStyle
-        )
+
+        /* inline resolveSpanStyleDefaults to avoid an allocation in draw */
+        val style = textLayoutResult.layoutInput.style.spanStyle
+        val textDecoration = style.textDecoration ?: TextDecoration.None
+        val shadow = style.shadow ?: Shadow.None
+        val drawStyle = style.drawStyle ?: Fill
         try {
-            val brush = resolvedSpanStyle.brush
+            val brush = style.brush
             if (brush != null) {
+                val alpha = if (style.textForegroundStyle !== Unspecified) {
+                    style.textForegroundStyle.alpha
+                } else {
+                    1.0f
+                }
                 textLayoutResult.multiParagraph.paint(
                     canvas = canvas,
                     brush = brush,
-                    alpha = resolvedSpanStyle.alpha,
-                    shadow = resolvedSpanStyle.shadow,
-                    decoration = resolvedSpanStyle.textDecoration,
-                    drawStyle = resolvedSpanStyle.drawStyle
+                    alpha = alpha,
+                    shadow = shadow,
+                    decoration = textDecoration,
+                    drawStyle = drawStyle
                 )
             } else {
+                val color = if (style.textForegroundStyle !== Unspecified) {
+                    style.textForegroundStyle.color
+                } else {
+                    Color.Black
+                }
                 textLayoutResult.multiParagraph.paint(
                     canvas = canvas,
-                    color = resolvedSpanStyle.color,
-                    shadow = resolvedSpanStyle.shadow,
-                    decoration = resolvedSpanStyle.textDecoration,
-                    drawStyle = resolvedSpanStyle.drawStyle
+                    color = color,
+                    shadow = shadow,
+                    decoration = textDecoration,
+                    drawStyle = drawStyle
                 )
             }
         } finally {
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt
index 309e339..ca8e2c9 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt
@@ -72,36 +72,12 @@
         )
     )
 
-    /**
-     * Styling configuration for a `Text`.
-     *
-     * @sample androidx.compose.ui.text.samples.TextStyleSample
-     *
-     * @param color The text color.
-     * @param fontSize The size of glyphs to use when painting the text. This
-     * may be [TextUnit.Unspecified] for inheriting from another [TextStyle].
-     * @param fontWeight The typeface thickness to use when painting the text (e.g., bold).
-     * @param fontStyle The typeface variant to use when drawing the letters (e.g., italic).
-     * @param fontSynthesis Whether to synthesize font weight and/or style when the requested weight
-     * or style cannot be found in the provided font family.
-     * @param fontFamily The font family to be used when rendering the text.
-     * @param fontFeatureSettings The advanced typography settings provided by font. The format is
-     * the same as the CSS font-feature-settings attribute:
-     * https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop
-     * @param letterSpacing The amount of space to add between each letter.
-     * @param baselineShift The amount by which the text is shifted up from the current baseline.
-     * @param textGeometricTransform The geometric transformation applied the text.
-     * @param localeList The locale list used to select region-specific glyphs.
-     * @param background The background color for the text.
-     * @param textDecoration The decorations to paint on the text (e.g., an underline).
-     * @param shadow The shadow effect applied on the text.
-     * @param textAlign The alignment of the text within the lines of the paragraph.
-     * @param textDirection The algorithm to be used to resolve the final text and paragraph
-     * direction: Left To Right or Right To Left. If no value is provided the system will use the
-     * [LayoutDirection] as the primary signal.
-     * @param lineHeight Line height for the [Paragraph] in [TextUnit] unit, e.g. SP or EM.
-     * @param textIndent The indentation of the paragraph.
-     */
+    @Deprecated(
+        "TextStyle constructors that do not take new stable parameters " +
+            "like LineHeightStyle, LineBreak, Hyphens are deprecated. Please use the new stable " +
+            "constructor.",
+        level = DeprecationLevel.HIDDEN
+    )
     @OptIn(ExperimentalTextApi::class)
     constructor(
         color: Color = Color.Unspecified,
@@ -155,42 +131,12 @@
         platformStyle = null
     )
 
-    /**
-     * Styling configuration for a `Text`.
-     *
-     * @sample androidx.compose.ui.text.samples.TextStyleSample
-     *
-     * @param color The text color.
-     * @param fontSize The size of glyphs to use when painting the text. This
-     * may be [TextUnit.Unspecified] for inheriting from another [TextStyle].
-     * @param fontWeight The typeface thickness to use when painting the text (e.g., bold).
-     * @param fontStyle The typeface variant to use when drawing the letters (e.g., italic).
-     * @param fontSynthesis Whether to synthesize font weight and/or style when the requested weight
-     * or style cannot be found in the provided font family.
-     * @param fontFamily The font family to be used when rendering the text.
-     * @param fontFeatureSettings The advanced typography settings provided by font. The format is
-     * the same as the CSS font-feature-settings attribute:
-     * https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop
-     * @param letterSpacing The amount of space to add between each letter.
-     * @param baselineShift The amount by which the text is shifted up from the current baseline.
-     * @param textGeometricTransform The geometric transformation applied the text.
-     * @param localeList The locale list used to select region-specific glyphs.
-     * @param background The background color for the text.
-     * @param textDecoration The decorations to paint on the text (e.g., an underline).
-     * @param shadow The shadow effect applied on the text.
-     * @param textAlign The alignment of the text within the lines of the paragraph.
-     * @param textDirection The algorithm to be used to resolve the final text and paragraph
-     * direction: Left To Right or Right To Left. If no value is provided the system will use the
-     * [LayoutDirection] as the primary signal.
-     * @param lineHeight Line height for the [Paragraph] in [TextUnit] unit, e.g. SP or EM.
-     * @param textIndent The indentation of the paragraph.
-     * @param platformStyle Platform specific [TextStyle] parameters.
-     * @param lineHeightStyle the configuration for line height such as vertical alignment of the
-     * line, whether to apply additional space as a result of line height to top of first line top
-     * and bottom of last line. The configuration is applied only when a [lineHeight] is defined.
-     * When null, [LineHeightStyle.Default] is used.
-     */
-    // TODO(b/246715337, b/245939557): Deprecate this when Hyphens and LineBreak are stable
+    @Deprecated(
+        "TextStyle constructors that do not take new stable parameters " +
+            "like LineHeightStyle, LineBreak, Hyphens are deprecated. Please use the new stable " +
+            "constructor.",
+        level = DeprecationLevel.HIDDEN
+    )
     @OptIn(ExperimentalTextApi::class)
     constructor(
         color: Color = Color.Unspecified,
@@ -269,6 +215,97 @@
      * @param background The background color for the text.
      * @param textDecoration The decorations to paint on the text (e.g., an underline).
      * @param shadow The shadow effect applied on the text.
+     * @param textAlign The alignment of the text within the lines of the paragraph.
+     * @param textDirection The algorithm to be used to resolve the final text and paragraph
+     * direction: Left To Right or Right To Left. If no value is provided the system will use the
+     * [LayoutDirection] as the primary signal.
+     * @param lineHeight Line height for the [Paragraph] in [TextUnit] unit, e.g. SP or EM.
+     * @param textIndent The indentation of the paragraph.
+     * @param platformStyle Platform specific [TextStyle] parameters.
+     * @param lineHeightStyle the configuration for line height such as vertical alignment of the
+     * line, whether to apply additional space as a result of line height to top of first line top
+     * and bottom of last line. The configuration is applied only when a [lineHeight] is defined.
+     * When null, [LineHeightStyle.Default] is used.
+     * @param lineBreak The line breaking configuration for the text.
+     * @param hyphens The configuration of hyphenation.
+     */
+    constructor(
+        color: Color = Color.Unspecified,
+        fontSize: TextUnit = TextUnit.Unspecified,
+        fontWeight: FontWeight? = null,
+        fontStyle: FontStyle? = null,
+        fontSynthesis: FontSynthesis? = null,
+        fontFamily: FontFamily? = null,
+        fontFeatureSettings: String? = null,
+        letterSpacing: TextUnit = TextUnit.Unspecified,
+        baselineShift: BaselineShift? = null,
+        textGeometricTransform: TextGeometricTransform? = null,
+        localeList: LocaleList? = null,
+        background: Color = Color.Unspecified,
+        textDecoration: TextDecoration? = null,
+        shadow: Shadow? = null,
+        textAlign: TextAlign? = null,
+        textDirection: TextDirection? = null,
+        lineHeight: TextUnit = TextUnit.Unspecified,
+        textIndent: TextIndent? = null,
+        platformStyle: PlatformTextStyle? = null,
+        lineHeightStyle: LineHeightStyle? = null,
+        lineBreak: LineBreak? = null,
+        hyphens: Hyphens? = null
+    ) : this(
+        SpanStyle(
+            color = color,
+            fontSize = fontSize,
+            fontWeight = fontWeight,
+            fontStyle = fontStyle,
+            fontSynthesis = fontSynthesis,
+            fontFamily = fontFamily,
+            fontFeatureSettings = fontFeatureSettings,
+            letterSpacing = letterSpacing,
+            baselineShift = baselineShift,
+            textGeometricTransform = textGeometricTransform,
+            localeList = localeList,
+            background = background,
+            textDecoration = textDecoration,
+            shadow = shadow,
+            platformStyle = platformStyle?.spanStyle
+        ),
+        ParagraphStyle(
+            textAlign = textAlign,
+            textDirection = textDirection,
+            lineHeight = lineHeight,
+            textIndent = textIndent,
+            platformStyle = platformStyle?.paragraphStyle,
+            lineHeightStyle = lineHeightStyle,
+            lineBreak = lineBreak,
+            hyphens = hyphens
+        ),
+        platformStyle = platformStyle
+    )
+
+    /**
+     * Styling configuration for a `Text`.
+     *
+     * @sample androidx.compose.ui.text.samples.TextStyleSample
+     *
+     * @param color The text color.
+     * @param fontSize The size of glyphs to use when painting the text. This
+     * may be [TextUnit.Unspecified] for inheriting from another [TextStyle].
+     * @param fontWeight The typeface thickness to use when painting the text (e.g., bold).
+     * @param fontStyle The typeface variant to use when drawing the letters (e.g., italic).
+     * @param fontSynthesis Whether to synthesize font weight and/or style when the requested weight
+     * or style cannot be found in the provided font family.
+     * @param fontFamily The font family to be used when rendering the text.
+     * @param fontFeatureSettings The advanced typography settings provided by font. The format is
+     * the same as the CSS font-feature-settings attribute:
+     * https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop
+     * @param letterSpacing The amount of space to add between each letter.
+     * @param baselineShift The amount by which the text is shifted up from the current baseline.
+     * @param textGeometricTransform The geometric transformation applied the text.
+     * @param localeList The locale list used to select region-specific glyphs.
+     * @param background The background color for the text.
+     * @param textDecoration The decorations to paint on the text (e.g., an underline).
+     * @param shadow The shadow effect applied on the text.
      * @param drawStyle Drawing style of text, whether fill in the text while drawing or stroke
      * around the edges.
      * @param textAlign The alignment of the text within the lines of the paragraph.
@@ -517,6 +554,12 @@
     @Stable
     operator fun plus(other: SpanStyle): TextStyle = this.merge(other)
 
+    @Deprecated(
+        "TextStyle copy constructors that do not take new stable parameters " +
+            "like LineHeightStyle, LineBreak, Hyphens are deprecated. Please use the new stable " +
+            "copy constructor.",
+        level = DeprecationLevel.HIDDEN
+    )
     @OptIn(ExperimentalTextApi::class)
     fun copy(
         color: Color = this.spanStyle.color,
@@ -576,7 +619,12 @@
         )
     }
 
-    // TODO(b/246715337, b/245939557): Deprecate this when Hyphens and LineBreak are stable
+    @Deprecated(
+        "TextStyle copy constructors that do not take new stable parameters " +
+            "like LineHeightStyle, LineBreak, Hyphens are deprecated. Please use the new stable " +
+            "copy constructor.",
+        level = DeprecationLevel.HIDDEN
+    )
     @OptIn(ExperimentalTextApi::class)
     fun copy(
         color: Color = this.spanStyle.color,
@@ -638,6 +686,69 @@
         )
     }
 
+    @OptIn(ExperimentalTextApi::class)
+    fun copy(
+        color: Color = this.spanStyle.color,
+        fontSize: TextUnit = this.spanStyle.fontSize,
+        fontWeight: FontWeight? = this.spanStyle.fontWeight,
+        fontStyle: FontStyle? = this.spanStyle.fontStyle,
+        fontSynthesis: FontSynthesis? = this.spanStyle.fontSynthesis,
+        fontFamily: FontFamily? = this.spanStyle.fontFamily,
+        fontFeatureSettings: String? = this.spanStyle.fontFeatureSettings,
+        letterSpacing: TextUnit = this.spanStyle.letterSpacing,
+        baselineShift: BaselineShift? = this.spanStyle.baselineShift,
+        textGeometricTransform: TextGeometricTransform? = this.spanStyle.textGeometricTransform,
+        localeList: LocaleList? = this.spanStyle.localeList,
+        background: Color = this.spanStyle.background,
+        textDecoration: TextDecoration? = this.spanStyle.textDecoration,
+        shadow: Shadow? = this.spanStyle.shadow,
+        textAlign: TextAlign? = this.paragraphStyle.textAlign,
+        textDirection: TextDirection? = this.paragraphStyle.textDirection,
+        lineHeight: TextUnit = this.paragraphStyle.lineHeight,
+        textIndent: TextIndent? = this.paragraphStyle.textIndent,
+        platformStyle: PlatformTextStyle? = this.platformStyle,
+        lineHeightStyle: LineHeightStyle? = this.paragraphStyle.lineHeightStyle,
+        lineBreak: LineBreak? = this.paragraphStyle.lineBreak,
+        hyphens: Hyphens? = this.paragraphStyle.hyphens
+    ): TextStyle {
+        return TextStyle(
+            spanStyle = SpanStyle(
+                textForegroundStyle = if (color == this.spanStyle.color) {
+                    spanStyle.textForegroundStyle
+                } else {
+                    TextForegroundStyle.from(color)
+                },
+                fontSize = fontSize,
+                fontWeight = fontWeight,
+                fontStyle = fontStyle,
+                fontSynthesis = fontSynthesis,
+                fontFamily = fontFamily,
+                fontFeatureSettings = fontFeatureSettings,
+                letterSpacing = letterSpacing,
+                baselineShift = baselineShift,
+                textGeometricTransform = textGeometricTransform,
+                localeList = localeList,
+                background = background,
+                textDecoration = textDecoration,
+                shadow = shadow,
+                platformStyle = platformStyle?.spanStyle,
+                drawStyle = this.drawStyle
+            ),
+            paragraphStyle = ParagraphStyle(
+                textAlign = textAlign,
+                textDirection = textDirection,
+                lineHeight = lineHeight,
+                textIndent = textIndent,
+                platformStyle = platformStyle?.paragraphStyle,
+                lineHeightStyle = lineHeightStyle,
+                lineBreak = lineBreak,
+                hyphens = hyphens,
+                textMotion = this.textMotion
+            ),
+            platformStyle = platformStyle
+        )
+    }
+
     @ExperimentalTextApi
     fun copy(
         color: Color = this.spanStyle.color,
@@ -901,17 +1012,11 @@
     /**
      * The hyphens configuration of the paragraph.
      */
-    @ExperimentalTextApi
-    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
-    @get:ExperimentalTextApi
     val hyphens: Hyphens? get() = this.paragraphStyle.hyphens
 
     /**
      * The line breaking configuration of the paragraph.
      */
-    @ExperimentalTextApi
-    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
-    @get:ExperimentalTextApi
     val lineBreak: LineBreak? get() = this.paragraphStyle.lineBreak
 
     /**
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 16bc6b0..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
@@ -16,8 +16,6 @@
 
 package androidx.compose.ui.text.style
 
-import androidx.compose.ui.text.ExperimentalTextApi
-
 /**
  * Automatic hyphenation configuration.
  *
@@ -43,8 +41,8 @@
  * The default configuration for [Hyphens] = [Hyphens.None]
  *
  */
-@ExperimentalTextApi
-class Hyphens private constructor() {
+@JvmInline
+value class Hyphens private constructor(internal val value: Int) {
     companion object {
         /**
          *  Lines will break with no hyphenation.
@@ -61,7 +59,7 @@
          * +---------+
          * </pre>
          */
-        val None = Hyphens()
+        val None = Hyphens(1)
 
         /**
          * The words will be automatically broken at appropriate hyphenation points.
@@ -76,7 +74,7 @@
          * +---------+
          * </pre>
          */
-        val Auto = Hyphens()
+        val Auto = Hyphens(2)
     }
 
     override fun toString() = when (this) {
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineBreak.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineBreak.kt
index 337f6af9..d25b7e7 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineBreak.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineBreak.kt
@@ -17,7 +17,9 @@
 package androidx.compose.ui.text.style
 
 import androidx.compose.runtime.Immutable
-import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.style.LineBreak.Companion.Heading
+import androidx.compose.ui.text.style.LineBreak.Companion.Paragraph
+import androidx.compose.ui.text.style.LineBreak.Companion.Simple
 
 /**
  * When soft wrap is enabled and the width of the text exceeds the width of its container,
@@ -38,7 +40,6 @@
  *
  * @sample androidx.compose.ui.text.samples.AndroidLineBreakSample
  */
-@ExperimentalTextApi
 @Immutable
 expect class LineBreak {
     companion object {
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/skikoMain/kotlin/androidx/compose/ui/text/style/LineBreak.skiko.kt b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/style/LineBreak.skiko.kt
index 21b4e2c..f611ea1 100644
--- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/style/LineBreak.skiko.kt
+++ b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/style/LineBreak.skiko.kt
@@ -17,10 +17,8 @@
 package androidx.compose.ui.text.style
 
 import androidx.compose.runtime.Immutable
-import androidx.compose.ui.text.ExperimentalTextApi
 
 @Immutable
-@ExperimentalTextApi
 actual class LineBreak private constructor() {
     actual companion object {
         actual val Simple: LineBreak = LineBreak()
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 57e115b..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
         )
 
@@ -850,6 +850,35 @@
     }
 
     @Test
+    fun hasStringAnnotationTrue() {
+        val text = "Test"
+        val annotation = "Annotation"
+        val tag = "tag"
+        val buildResult = AnnotatedString.Builder().apply {
+            pushStringAnnotation(tag, annotation)
+            append(text)
+            pop()
+        }.toAnnotatedString()
+
+        assertThat(buildResult.hasStringAnnotations(tag, 0, text.length)).isTrue()
+    }
+
+    @Test
+    fun hasStringAnnotationFalse() {
+        val text = "Test"
+        val annotation = "Annotation"
+        val tag = "tag"
+        val buildResult = AnnotatedString.Builder().apply {
+            pushStringAnnotation(tag, annotation)
+            append(text)
+            pop()
+            append(text)
+        }.toAnnotatedString()
+
+        assertThat(buildResult.hasStringAnnotations(tag, text.length, buildResult.length)).isFalse()
+    }
+
+    @Test
     fun pushAnnotation_multiple_nested() {
         val annotation1 = "Annotation1"
         val annotation2 = "Annotation2"
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-text/src/test/java/androidx/compose/ui/text/ParagraphStyleTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/ParagraphStyleTest.kt
index 1eddda1..00d130c 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/ParagraphStyleTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/ParagraphStyleTest.kt
@@ -103,7 +103,6 @@
         assertThat(newStyle.textDirection).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge non-null hyphens uses other's hyphens`() {
         val style = ParagraphStyle(hyphens = Hyphens.Auto)
@@ -114,7 +113,6 @@
         assertThat(newStyle.hyphens).isEqualTo(otherStyle.hyphens)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge hyphens other null, returns original`() {
         val style = ParagraphStyle(hyphens = Hyphens.Auto)
@@ -125,7 +123,6 @@
         assertThat(newStyle.hyphens).isEqualTo(style.hyphens)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge null hyphens other non-null, returns other's hyphens`() {
         val style = ParagraphStyle(hyphens = null)
@@ -136,7 +133,6 @@
         assertThat(newStyle.hyphens).isEqualTo(otherStyle.hyphens)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge hyphens both null returns null`() {
         val style = ParagraphStyle(hyphens = null)
@@ -207,7 +203,6 @@
         assertThat(newStyle.textIndent).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge null with non-null lineBreak uses other's lineBreak`() {
         val style = ParagraphStyle(lineBreak = null)
@@ -218,7 +213,6 @@
         assertThat(mergedStyle.lineBreak).isEqualTo(otherStyle.lineBreak)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge non-null with null lineBreak returns original's lineBreak`() {
         val style = ParagraphStyle(lineBreak = LineBreak.Paragraph)
@@ -229,7 +223,6 @@
         assertThat(mergedStyle.lineBreak).isEqualTo(style.lineBreak)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge null with null lineBreak returns null`() {
         val style = ParagraphStyle(lineBreak = null)
@@ -240,7 +233,6 @@
         assertThat(mergedStyle.lineBreak).isEqualTo(null)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge non-null with non-null lineBreak returns other's lineBreak`() {
         val style = ParagraphStyle(lineBreak = LineBreak.Paragraph)
@@ -251,7 +243,6 @@
         assertThat(mergedStyle.lineBreak).isEqualTo(otherStyle.lineBreak)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge null platformStyles`() {
         val style1 = ParagraphStyle(platformStyle = null)
@@ -339,7 +330,6 @@
         assertThat(newStyle.textDirection).isEqualTo(style2.textDirection)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp hyphens with a null, b not null and t is smaller than half`() {
         val style1 = ParagraphStyle(hyphens = null)
@@ -350,7 +340,6 @@
         assertThat(newStyle.hyphens).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp hyphens with a null, b not null and t is equal to half`() {
         val style1 = ParagraphStyle(hyphens = null)
@@ -361,7 +350,6 @@
         assertThat(newStyle.hyphens).isEqualTo(style2.hyphens)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp hyphens with a and b are not null and t is smaller than half`() {
         val style1 = ParagraphStyle(hyphens = Hyphens.Auto)
@@ -372,7 +360,6 @@
         assertThat(newStyle.hyphens).isEqualTo(style1.hyphens)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp hyphens with a and b are not Null and t is larger than half`() {
         val style1 = ParagraphStyle(hyphens = Hyphens.Auto)
@@ -443,7 +430,6 @@
         assertThat(anotherNewStyle.lineHeight).isEqualTo(22.sp)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp with null platformStyles has null platformStyle`() {
         val style = ParagraphStyle(platformStyle = null)
@@ -454,7 +440,6 @@
         assertThat(lerpedStyle.platformStyle).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp with null lineHeightStyles has null lineHeightStyle`() {
         val style = ParagraphStyle(lineHeightStyle = null)
@@ -465,7 +450,6 @@
         assertThat(lerpedStyle.lineHeightStyle).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp with non-null start, null end, closer to start has non-null lineHeightStyle`() {
         val style = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
@@ -476,7 +460,6 @@
         assertThat(lerpedStyle.lineHeightStyle).isSameInstanceAs(style.lineHeightStyle)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp with non-null start, null end, closer to end has null lineHeightStyle`() {
         val style = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
@@ -487,7 +470,6 @@
         assertThat(lerpedStyle.lineHeightStyle).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp with null start, non-null end, closer to start has null lineHeightStyle`() {
         val style = ParagraphStyle(lineHeightStyle = null)
@@ -498,7 +480,6 @@
         assertThat(lerpedStyle.lineHeightStyle).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp with null start, non-null end, closer to end has non-null lineHeightStyle`() {
         val style = ParagraphStyle(lineHeightStyle = null)
@@ -509,7 +490,6 @@
         assertThat(lerpedStyle.lineHeightStyle).isSameInstanceAs(otherStyle.lineHeightStyle)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp with non-null start, null end, closer to start has non-null lineBreak`() {
         val style = ParagraphStyle(lineBreak = LineBreak.Heading)
@@ -520,7 +500,6 @@
         assertThat(lerpedStyle.lineBreak).isSameInstanceAs(style.lineBreak)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp with non-null start, null end, closer to end has null lineBreak`() {
         val style = ParagraphStyle(lineBreak = LineBreak.Heading)
@@ -531,7 +510,6 @@
         assertThat(lerpedStyle.lineBreak).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp with null start, non-null end, closer to start has null lineBreak`() {
         val style = ParagraphStyle(lineHeightStyle = null)
@@ -542,7 +520,6 @@
         assertThat(lerpedStyle.lineBreak).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp with null start, non-null end, closer to end has non-null lineBreak`() {
         val style = ParagraphStyle(lineBreak = null)
@@ -553,7 +530,6 @@
         assertThat(lerpedStyle.lineBreak).isSameInstanceAs(otherStyle.lineBreak)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `equals return false for different line height behavior`() {
         val style = ParagraphStyle(lineHeightStyle = null)
@@ -562,7 +538,6 @@
         assertThat(style == otherStyle).isFalse()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `equals return true for same line height behavior`() {
         val style = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
@@ -571,7 +546,6 @@
         assertThat(style == otherStyle).isTrue()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `hashCode is same for same line height behavior`() {
         val style = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
@@ -580,7 +554,6 @@
         assertThat(style.hashCode()).isEqualTo(otherStyle.hashCode())
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `hashCode is different for different line height behavior`() {
         val style = ParagraphStyle(
@@ -599,7 +572,6 @@
         assertThat(style.hashCode()).isNotEqualTo(otherStyle.hashCode())
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `copy with lineHeightStyle returns new lineHeightStyle`() {
         val style = ParagraphStyle(
@@ -617,7 +589,6 @@
         assertThat(newStyle.lineHeightStyle).isEqualTo(newLineHeightStyle)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `copy without lineHeightStyle uses existing lineHeightStyle`() {
         val style = ParagraphStyle(
@@ -631,7 +602,6 @@
         assertThat(newStyle.lineHeightStyle).isEqualTo(style.lineHeightStyle)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `copy with hyphens returns new hyphens`() {
         val style = ParagraphStyle(hyphens = Hyphens.None)
@@ -640,7 +610,6 @@
         assertThat(newStyle.hyphens).isEqualTo(Hyphens.Auto)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `copy without hyphens uses existing hyphens`() {
         val style = ParagraphStyle(hyphens = Hyphens.Auto)
@@ -649,7 +618,6 @@
         assertThat(newStyle.hyphens).isEqualTo(style.hyphens)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `equals return false for different and non-null hyphens behavior`() {
         val style = ParagraphStyle(hyphens = Hyphens.None)
@@ -658,7 +626,6 @@
         assertThat(style == otherStyle).isFalse()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `equals return false for null and non-null hyphens`() {
         val style = ParagraphStyle(hyphens = null)
@@ -667,7 +634,6 @@
         assertThat(style == otherStyle).isFalse()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `equals return true for same and non-null hyphens behavior`() {
         val style = ParagraphStyle(hyphens = Hyphens.Auto)
@@ -676,7 +642,6 @@
         assertThat(style == otherStyle).isTrue()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `equals return true for both null hyphens`() {
         val style = ParagraphStyle(hyphens = null)
@@ -685,7 +650,6 @@
         assertThat(style == otherStyle).isTrue()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `hashCode is same for same hyphens behavior`() {
         val style = ParagraphStyle(hyphens = Hyphens.Auto)
@@ -694,7 +658,6 @@
         assertThat(style.hashCode()).isEqualTo(otherStyle.hashCode())
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `hashCode is different for different hyphens behavior`() {
         val style = ParagraphStyle(hyphens = Hyphens.None)
@@ -703,7 +666,6 @@
         assertThat(style.hashCode()).isNotEqualTo(otherStyle.hashCode())
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge with null lineHeightStyle uses other's lineHeightStyle`() {
         val style = ParagraphStyle(lineHeightStyle = null)
@@ -714,7 +676,6 @@
         assertThat(newStyle.lineHeightStyle).isEqualTo(otherStyle.lineHeightStyle)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge with non-null lineHeightStyle, returns original`() {
         val style = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
@@ -725,7 +686,6 @@
         assertThat(newStyle.lineHeightStyle).isEqualTo(style.lineHeightStyle)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge with both null lineHeightStyle returns null`() {
         val style = ParagraphStyle(lineHeightStyle = null)
@@ -736,7 +696,6 @@
         assertThat(newStyle.lineHeightStyle).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge with both non-null lineHeightStyle returns other's lineHeightStyle`() {
         val style = ParagraphStyle(
@@ -757,7 +716,6 @@
         assertThat(newStyle.lineHeightStyle).isEqualTo(otherStyle.lineHeightStyle)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `constructor without lineHeightStyle sets lineHeightStyle to null`() {
         val style = ParagraphStyle(textAlign = TextAlign.Start)
@@ -765,7 +723,6 @@
         assertThat(style.lineHeightStyle).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `copy with lineBreak returns new lineBreak`() {
         val style = ParagraphStyle(lineBreak = LineBreak.Paragraph)
@@ -774,7 +731,6 @@
         assertThat(newStyle.lineBreak).isEqualTo(LineBreak.Heading)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `copy without lineBreak uses existing lineBreak`() {
         val style = ParagraphStyle(lineBreak = LineBreak.Paragraph)
@@ -783,7 +739,6 @@
         assertThat(newStyle.lineBreak).isEqualTo(style.lineBreak)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `hashCode is same for same lineBreak`() {
         val style = ParagraphStyle(lineBreak = LineBreak.Paragraph)
@@ -792,7 +747,6 @@
         assertThat(style.hashCode()).isEqualTo(otherStyle.hashCode())
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `hashCode is different for different lineBreak`() {
         val style = ParagraphStyle(lineBreak = LineBreak.Paragraph)
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleTest.kt
index 4c75397..5a04d87 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleTest.kt
@@ -77,7 +77,6 @@
         assertThat(style.textMotion).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `constructor with customized hyphens`() {
         val style = TextStyle(hyphens = Hyphens.Auto)
@@ -221,7 +220,6 @@
         assertThat(newStyle.textMotion).isEqualTo(TextMotion.Static)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `copy with lineBreak returns new lineBreak`() {
         val style = TextStyle(lineBreak = LineBreak.Paragraph)
@@ -231,7 +229,6 @@
         assertThat(newStyle.lineBreak).isEqualTo(LineBreak.Heading)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `copy without lineBreak uses existing lineBreak`() {
         val style = TextStyle(lineBreak = LineBreak.Paragraph)
@@ -342,7 +339,6 @@
         assertThat(style.fontFamily).isEqualTo(fontFamily)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge with other's hyphens is null should use this hyphens`() {
         val style = TextStyle(hyphens = Hyphens.Auto)
@@ -353,7 +349,6 @@
         assertThat(newStyle.hyphens).isEqualTo(style.hyphens)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge with other's hyphens is set should use other's hyphens`() {
         val style = TextStyle(hyphens = Hyphens.Auto)
@@ -364,7 +359,6 @@
         assertThat(newStyle.hyphens).isEqualTo(otherStyle.hyphens)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `constructor with customized lineBreak`() {
         val style = TextStyle(lineBreak = LineBreak.Heading)
@@ -755,7 +749,6 @@
         assertThat(newStyle.textIndent).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge with null platformStyles null has null platformStyle`() {
         val style = TextStyle(platformStyle = null)
@@ -824,7 +817,6 @@
         assertThat(mergedStyle.alpha).isEqualTo(0.3f)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge null and non-null lineBreak uses other's lineBreak`() {
         val style = TextStyle(lineBreak = null)
@@ -835,7 +827,6 @@
         assertThat(mergedStyle.lineBreak).isEqualTo(otherStyle.lineBreak)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge non-null and null lineBreak uses original`() {
         val style = TextStyle(lineBreak = LineBreak.Paragraph)
@@ -846,7 +837,6 @@
         assertThat(mergedStyle.lineBreak).isEqualTo(style.lineBreak)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge with both null lineBreak uses null`() {
         val style = TextStyle(lineBreak = null)
@@ -857,7 +847,6 @@
         assertThat(mergedStyle.lineBreak).isEqualTo(null)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge with both non-null lineBreak uses other's lineBreak`() {
         val style = TextStyle(lineBreak = LineBreak.Paragraph)
@@ -993,7 +982,6 @@
         assertThat(newStyle.color).isEqualTo(lerp(start = color1, stop = color2, fraction = t))
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp hyphens with a and b are not Null and t is smaller than half`() {
         val hyphens1 = Hyphens.Auto
@@ -1007,7 +995,6 @@
         assertThat(newStyle.hyphens).isEqualTo(hyphens1)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp hyphens with a and b are not Null and t is larger than half`() {
         val hyphens1 = Hyphens.Auto
@@ -1439,7 +1426,6 @@
         assertThat(newStyle.lineHeight).isEqualTo(TextUnit.Unspecified)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp with null platformStyles has null platformStyle`() {
         val style = TextStyle(platformStyle = null)
@@ -1450,7 +1436,6 @@
         assertThat(lerpedStyle.platformStyle).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `constructor without platformStyle sets platformStyle to null`() {
         val style = TextStyle(textAlign = TextAlign.Start)
@@ -1458,7 +1443,6 @@
         assertThat(style.platformStyle).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `copy without platformStyle uses existing platformStyle`() {
         @Suppress("DEPRECATION")
@@ -1509,7 +1493,6 @@
         assertThat(newStyle.color).isEqualTo(Color.Unspecified)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp with non-null start, null end, closer to start has non-null lineBreak`() {
         val style = TextStyle(lineBreak = LineBreak.Heading)
@@ -1520,7 +1503,6 @@
         assertThat(lerpedStyle.lineBreak).isSameInstanceAs(style.lineBreak)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp with non-null start, null end, closer to end has null lineBreak`() {
         val style = TextStyle(lineBreak = LineBreak.Heading)
@@ -1531,7 +1513,6 @@
         assertThat(lerpedStyle.lineBreak).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp with null start, non-null end, closer to start has null lineBreak`() {
         val style = TextStyle(lineHeightStyle = null)
@@ -1542,7 +1523,6 @@
         assertThat(lerpedStyle.lineBreak).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lerp with null start, non-null end, closer to end has non-null lineBreak`() {
         val style = TextStyle(lineBreak = null)
@@ -1758,7 +1738,6 @@
         TextStyle(lineHeight = (-1).sp)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lineHeightStyle lerp with null lineHeightStyles has null lineHeightStyle`() {
         val style = TextStyle(lineHeightStyle = null)
@@ -1769,7 +1748,6 @@
         assertThat(lerpedStyle.lineHeightStyle).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lineHeightStyle lerp with non-null start, null end, closer to start has non-null`() {
         val style = TextStyle(lineHeightStyle = LineHeightStyle.Default)
@@ -1780,7 +1758,6 @@
         assertThat(lerpedStyle.lineHeightStyle).isSameInstanceAs(style.lineHeightStyle)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lineHeightStyle lerp with non-null start, null end, closer to end has null`() {
         val style = TextStyle(lineHeightStyle = LineHeightStyle.Default)
@@ -1791,7 +1768,6 @@
         assertThat(lerpedStyle.lineHeightStyle).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lineHeightStyle lerp with null start, non-null end, closer to start has null`() {
         val style = TextStyle(lineHeightStyle = null)
@@ -1802,7 +1778,6 @@
         assertThat(lerpedStyle.lineHeightStyle).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `lineHeightStyle lerp with null start, non-null end, closer to end has non-null`() {
         val style = TextStyle(lineHeightStyle = null)
@@ -1813,7 +1788,6 @@
         assertThat(lerpedStyle.lineHeightStyle).isSameInstanceAs(otherStyle.lineHeightStyle)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `equals return false for different line height behavior`() {
         val style = TextStyle(lineHeightStyle = null)
@@ -1822,7 +1796,6 @@
         assertThat(style == otherStyle).isFalse()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `equals return true for same line height behavior`() {
         val style = TextStyle(lineHeightStyle = LineHeightStyle.Default)
@@ -1831,7 +1804,6 @@
         assertThat(style == otherStyle).isTrue()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `hashCode is same for same line height behavior`() {
         val style = TextStyle(lineHeightStyle = LineHeightStyle.Default)
@@ -1840,7 +1812,6 @@
         assertThat(style.hashCode()).isEqualTo(otherStyle.hashCode())
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `hashCode is different for different line height behavior`() {
         val style = TextStyle(
@@ -1859,7 +1830,6 @@
         assertThat(style.hashCode()).isNotEqualTo(otherStyle.hashCode())
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `copy with lineHeightStyle returns new lineHeightStyle`() {
         val style = TextStyle(
@@ -1877,7 +1847,6 @@
         assertThat(newStyle.lineHeightStyle).isEqualTo(newLineHeightStyle)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `copy without lineHeightStyle uses existing lineHeightStyle`() {
         val style = TextStyle(
@@ -1891,7 +1860,6 @@
         assertThat(newStyle.lineHeightStyle).isEqualTo(style.lineHeightStyle)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge with null lineHeightStyle uses other's lineHeightStyle`() {
         val style = TextStyle(lineHeightStyle = null)
@@ -1902,7 +1870,6 @@
         assertThat(newStyle.lineHeightStyle).isEqualTo(otherStyle.lineHeightStyle)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge with non-null lineHeightStyle, returns original`() {
         val style = TextStyle(lineHeightStyle = LineHeightStyle.Default)
@@ -1913,7 +1880,6 @@
         assertThat(newStyle.lineHeightStyle).isEqualTo(style.lineHeightStyle)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge with both null lineHeightStyle returns null`() {
         val style = TextStyle(lineHeightStyle = null)
@@ -1924,7 +1890,6 @@
         assertThat(newStyle.lineHeightStyle).isNull()
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `merge with both non-null lineHeightStyle returns other's lineHeightStyle`() {
         val style = TextStyle(
@@ -1945,7 +1910,6 @@
         assertThat(newStyle.lineHeightStyle).isEqualTo(otherStyle.lineHeightStyle)
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun `constructor without lineHeightStyle sets lineHeightStyle to null`() {
         val style = TextStyle(textAlign = TextAlign.Start)
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index 0cae782..ee89714d 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -38,9 +38,10 @@
         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"))
 
         // kotlin-reflect and animation-tooling-internal are provided by Studio at runtime
         compileOnly(project(":compose:animation:animation-tooling-internal"))
@@ -87,7 +88,7 @@
                 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"))
 
                 // kotlin-reflect and tooling-animation-internal are provided by Studio at runtime
                 compileOnly(project(":compose:animation:animation-tooling-internal"))
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
index 59dc482..22efa75 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
@@ -239,8 +239,11 @@
     }
 
     @Test
-    fun animatedContentIsNotSubscribed() {
-        checkAnimationsAreSubscribed("AnimatedContentPreview")
+    fun animatedContentIsSubscribed() {
+        checkAnimationsAreSubscribed(
+            "AnimatedContentPreview",
+            animatedContent = listOf("AnimatedContent")
+        )
     }
 
     @Test
@@ -369,9 +372,10 @@
         UnsupportedComposeAnimation.testOverrideAvailability(false)
         checkAnimationsAreSubscribed(
             "AllAnimations",
-            emptyList(),
-            listOf("checkBoxAnim", "Crossfade"),
+            unsupported = emptyList(),
+            transitions = listOf("checkBoxAnim", "Crossfade"),
             animateXAsState = listOf("DpAnimation", "IntAnimation"),
+            animatedContent = listOf("AnimatedContent"),
             infiniteTransitions = listOf("InfiniteTransition")
         )
         UnsupportedComposeAnimation.testOverrideAvailability(true)
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt
index acab459..7dbb768 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt
@@ -34,12 +34,15 @@
 import androidx.compose.ui.tooling.TransitionAnimatedVisibilityPreview
 import androidx.compose.ui.tooling.TransitionPreview
 import androidx.compose.ui.tooling.animation.InfiniteTransitionComposeAnimation.Companion.parse
+import androidx.compose.ui.tooling.animation.Utils.searchAndTrackAllAnimations
 import androidx.compose.ui.tooling.animation.Utils.searchForAnimation
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import org.junit.Assert
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -56,6 +59,7 @@
         var callbacks = 0
         val search = AnimationSearch.TargetBasedSearch { callbacks++ }
         rule.searchForAnimation(search) { TargetBasedAnimationPreview() }
+        assertTrue(search.hasAnimations())
         assertEquals(1, search.animations.size)
         search.track()
         assertEquals(1, callbacks)
@@ -63,10 +67,18 @@
     }
 
     @Test
+    fun targetBasedAnimationIsFoundButNotSupported() {
+        val search = AnimationSearch({ PreviewAnimationClock {} }) { }
+        rule.searchAndTrackAllAnimations(search) { TargetBasedAnimationPreview() }
+        assertFalse(search.hasAnimations)
+    }
+
+    @Test
     fun decayAnimationIsFound() {
         var callbacks = 0
         val search = AnimationSearch.DecaySearch { callbacks++ }
         rule.searchForAnimation(search) { DecayAnimationPreview() }
+        assertTrue(search.hasAnimations())
         assertEquals(1, search.animations.size)
         search.track()
         assertEquals(1, callbacks)
@@ -74,10 +86,18 @@
     }
 
     @Test
+    fun decayAnimationIsFoundButNotSupported() {
+        val search = AnimationSearch({ PreviewAnimationClock {} }) { }
+        rule.searchAndTrackAllAnimations(search) { DecayAnimationPreview() }
+        assertFalse(search.hasAnimations)
+    }
+
+    @Test
     fun infiniteTransitionIsFound() {
         var callbacks = 0
         val search = AnimationSearch.InfiniteTransitionSearch { callbacks++ }
         rule.searchForAnimation(search) { InfiniteTransitionPreview() }
+        assertTrue(search.hasAnimations())
         assertEquals(1, search.animations.size)
         search.track()
         assertEquals(1, callbacks)
@@ -90,6 +110,13 @@
     }
 
     @Test
+    fun infiniteTransitionIsFoundAndSupported() {
+        val search = AnimationSearch({ PreviewAnimationClock {} }) { }
+        rule.searchAndTrackAllAnimations(search) { InfiniteTransitionPreview() }
+        assertTrue(search.hasAnimations)
+    }
+
+    @Test
     fun multipleInfiniteTransitionIsFound() {
         val search = AnimationSearch.InfiniteTransitionSearch { }
         rule.searchForAnimation(search) {
@@ -100,6 +127,7 @@
             rememberInfiniteTransition()
         }
         assertEquals(5, search.animations.size)
+        assertTrue(search.hasAnimations())
     }
 
     @Test
@@ -107,15 +135,16 @@
         var callbacks = 0
         val search = AnimationSearch.AnimateXAsStateSearch { callbacks++ }
         rule.searchForAnimation(search) { AnimateAsStatePreview() }
+        assertTrue(search.hasAnimations())
         assertEquals(2, search.animations.size)
         search.animations.first().let {
-            Assert.assertTrue(it.animationSpec is SpringSpec)
+            assertTrue(it.animationSpec is SpringSpec)
             Assert.assertNotNull(it.toolingState)
             Assert.assertNotNull(it.animatable)
             assertEquals("IntAnimation", it.animatable.label)
         }
         search.animations.last().let {
-            Assert.assertTrue(it.animationSpec is SpringSpec)
+            assertTrue(it.animationSpec is SpringSpec)
             Assert.assertNotNull(it.toolingState)
             Assert.assertNotNull(it.animatable)
             assertEquals("DpAnimation", it.animatable.label)
@@ -127,19 +156,27 @@
     }
 
     @Test
+    fun animatedXAsStateSearchIsFoundAndSupported() {
+        val search = AnimationSearch({ PreviewAnimationClock {} }) { }
+        rule.searchAndTrackAllAnimations(search) { AnimateAsStatePreview() }
+        assertTrue(search.hasAnimations)
+    }
+
+    @Test
     fun animatedXAsStateWithLabelsSearchIsFound() {
         var callbacks = 0
         val search = AnimationSearch.AnimateXAsStateSearch { callbacks++ }
         rule.searchForAnimation(search) { AnimateAsStateWithLabelsPreview() }
+        assertTrue(search.hasAnimations())
         assertEquals(2, search.animations.size)
         search.animations.first().let {
-            Assert.assertTrue(it.animationSpec is SpringSpec)
+            assertTrue(it.animationSpec is SpringSpec)
             Assert.assertNotNull(it.toolingState)
             Assert.assertNotNull(it.animatable)
             assertEquals("CustomIntLabel", it.animatable.label)
         }
         search.animations.last().let {
-            Assert.assertTrue(it.animationSpec is SpringSpec)
+            assertTrue(it.animationSpec is SpringSpec)
             Assert.assertNotNull(it.toolingState)
             Assert.assertNotNull(it.animatable)
             assertEquals("CustomDpLabel", it.animatable.label)
@@ -151,20 +188,36 @@
     }
 
     @Test
+    fun animatedXAsStateWithLabelsSearchIsFoundAndSupported() {
+        val search = AnimationSearch({ PreviewAnimationClock {} }) { }
+        rule.searchAndTrackAllAnimations(search) { AnimateAsStateWithLabelsPreview() }
+        assertTrue(search.hasAnimations)
+    }
+
+    @Test
     fun animatedContentSizeIsFound() {
         var callbacks = 0
         val search = AnimationSearch.AnimateContentSizeSearch { callbacks++ }
         rule.searchForAnimation(search) { AnimateContentSizePreview() }
+        assertTrue(search.hasAnimations())
         assertEquals(1, search.animations.size)
         search.track()
         assertEquals(1, callbacks)
     }
 
     @Test
+    fun animatedContentSizeIsFoundButNotSupported() {
+        val search = AnimationSearch({ PreviewAnimationClock {} }) { }
+        rule.searchAndTrackAllAnimations(search) { AnimateContentSizePreview() }
+        assertFalse(search.hasAnimations)
+    }
+
+    @Test
     fun transitionIsFound() {
         var callbacks = 0
         val search = AnimationSearch.TransitionSearch { callbacks++ }
         rule.searchForAnimation(search) { TransitionPreview() }
+        assertTrue(search.hasAnimations())
         assertEquals(1, search.animations.size)
         search.track()
         assertEquals(1, callbacks)
@@ -172,6 +225,13 @@
     }
 
     @Test
+    fun transitionIsFoundAndSupported() {
+        val search = AnimationSearch({ PreviewAnimationClock {} }) { }
+        rule.searchAndTrackAllAnimations(search) { TransitionPreview() }
+        assertTrue(search.hasAnimations)
+    }
+
+    @Test
     fun animatedVisibilityExtensionIsFoundAsTransition() {
         var transitionSearchCallbacks = 0
         var animatedVisibilitySearchCallbacks = 0
@@ -181,6 +241,8 @@
         rule.searchForAnimation(transitionSearch, animatedVisibilitySearch) {
             TransitionAnimatedVisibilityPreview()
         }
+        assertTrue(transitionSearch.hasAnimations())
+        assertFalse(animatedVisibilitySearch.hasAnimations())
         assertEquals(1, transitionSearch.animations.size)
         assertEquals(0, animatedVisibilitySearch.animations.size)
         // Track animations.
@@ -191,10 +253,18 @@
     }
 
     @Test
+    fun animatedVisibilityExtensionIsFoundAndSupported() {
+        val search = AnimationSearch({ PreviewAnimationClock {} }) { }
+        rule.searchAndTrackAllAnimations(search) { TransitionAnimatedVisibilityPreview() }
+        assertTrue(search.hasAnimations)
+    }
+
+    @Test
     fun crossFadeIsFoundAsTransition() {
         var callbacks = 0
         val search = AnimationSearch.TransitionSearch { callbacks++ }
         rule.searchForAnimation(search) { CrossFadePreview() }
+        assertTrue(search.hasAnimations())
         assertEquals(1, search.animations.size)
         search.track()
         assertEquals(1, callbacks)
@@ -203,10 +273,18 @@
     }
 
     @Test
+    fun crossFadeIsFoundAsTransitionAndSupported() {
+        val search = AnimationSearch({ PreviewAnimationClock {} }) { }
+        rule.searchAndTrackAllAnimations(search) { CrossFadePreview() }
+        assertTrue(search.hasAnimations)
+    }
+
+    @Test
     fun crossFadeWithLabelIsFoundAsTransition() {
         var callbacks = 0
         val search = AnimationSearch.TransitionSearch { callbacks++ }
         rule.searchForAnimation(search) { CrossFadeWithLabelPreview() }
+        assertTrue(search.hasAnimations())
         assertEquals(1, search.animations.size)
         search.track()
         assertEquals(1, callbacks)
@@ -215,10 +293,18 @@
     }
 
     @Test
+    fun crossFadeWithLabelIsFoundAsTransitionAndSupported() {
+        val search = AnimationSearch({ PreviewAnimationClock {} }) { }
+        rule.searchAndTrackAllAnimations(search) { CrossFadeWithLabelPreview() }
+        assertTrue(search.hasAnimations)
+    }
+
+    @Test
     fun animatedVisibilityIsFound() {
         var callbacks = 0
         val search = AnimationSearch.AnimatedVisibilitySearch { callbacks++ }
         rule.searchForAnimation(search) { AnimatedVisibilityPreview() }
+        assertTrue(search.hasAnimations())
         assertEquals(1, search.animations.size)
         search.track()
         assertEquals(1, callbacks)
@@ -226,10 +312,18 @@
     }
 
     @Test
+    fun animatedVisibilityIsFoundAndSupported() {
+        val search = AnimationSearch({ PreviewAnimationClock {} }) { }
+        rule.searchAndTrackAllAnimations(search) { AnimatedVisibilityPreview() }
+        assertTrue(search.hasAnimations)
+    }
+
+    @Test
     fun animatedContentIsFound() {
         var callbacks = 0
         val search = AnimationSearch.AnimatedContentSearch { callbacks++ }
         rule.searchForAnimation(search) { AnimatedContentPreview() }
+        assertTrue(search.hasAnimations())
         assertEquals(1, search.animations.size)
         search.track()
         assertEquals(1, callbacks)
@@ -237,6 +331,13 @@
     }
 
     @Test
+    fun animatedContentIsFoundAndSupported() {
+        val search = AnimationSearch({ PreviewAnimationClock {} }) { }
+        rule.searchAndTrackAllAnimations(search) { AnimatedContentPreview() }
+        assertTrue(search.hasAnimations)
+    }
+
+    @Test
     fun animatedContentExtensionIsFoundAsTransition() {
         var transitionCallbacks = 0
         var animatedContentCallbacks = 0
@@ -246,6 +347,8 @@
         rule.searchForAnimation(transitionSearch, animatedContentSearch) {
             AnimatedContentExtensionPreview()
         }
+        assertTrue(transitionSearch.hasAnimations())
+        assertFalse(animatedContentSearch.hasAnimations())
         assertEquals(1, transitionSearch.animations.size)
         assertEquals(0, animatedContentSearch.animations.size)
         transitionSearch.track()
@@ -253,4 +356,11 @@
         assertEquals(1, transitionCallbacks)
         assertEquals(0, animatedContentCallbacks)
     }
+
+    @Test
+    fun animatedContentExtensionIsFoundAsTransitionAndSupported() {
+        val search = AnimationSearch({ PreviewAnimationClock {} }) { }
+        rule.searchAndTrackAllAnimations(search) { AnimatedContentExtensionPreview() }
+        assertTrue(search.hasAnimations)
+    }
 }
\ No newline at end of file
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/TransitionClockTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt
index 5f0b0be..27dc911 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClockTest.kt
@@ -21,7 +21,9 @@
 import androidx.compose.animation.Crossfade
 import androidx.compose.animation.ExperimentalAnimationApi
 import androidx.compose.animation.animateColor
+import androidx.compose.animation.core.ExperimentalTransitionApi
 import androidx.compose.animation.core.animateDp
+import androidx.compose.animation.core.createChildTransition
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.core.updateTransition
 import androidx.compose.animation.tooling.ComposeAnimatedProperty
@@ -30,6 +32,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.tooling.animation.AnimatedContentComposeAnimation.Companion.parseAnimatedContent
@@ -40,6 +43,7 @@
 import androidx.compose.ui.tooling.animation.states.ComposeAnimationState
 import androidx.compose.ui.tooling.animation.states.TargetState
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotNull
@@ -496,6 +500,68 @@
         return clock
     }
 
+    @Test
+    fun childTransition() {
+        val search = AnimationSearch.TransitionSearch { }
+        rule.searchForAnimation(search) { childTransitions() }
+        val clock = TransitionClock(search.animations.first().parse()!!)
+
+        rule.runOnIdle {
+            clock.getTransitions(100).let {
+                assertEquals(5, it.size)
+                assertEquals("Parent", it[0].label)
+                assertEquals("Child1", it[1].label)
+                assertEquals("Grandchild", it[2].label)
+                assertEquals("GrandGrandchild", it[3].label)
+                assertEquals("Child2", it[4].label)
+            }
+            clock.getAnimatedProperties().let {
+                assertEquals(5, it.size)
+                assertEquals("Parent", it[0].label)
+                assertEquals("Child1", it[1].label)
+                assertEquals("Grandchild", it[2].label)
+                assertEquals("GrandGrandchild", it[3].label)
+                assertEquals("Child2", it[4].label)
+            }
+        }
+    }
+
+    @OptIn(ExperimentalTransitionApi::class)
+    @Composable
+    fun childTransitions() {
+        val state by remember { mutableStateOf(EnumState.One) }
+        val parentTransition = updateTransition(state, label = "parent")
+        parentTransition.animateDp(
+            transitionSpec = { tween(durationMillis = 1000, delayMillis = 100) },
+            label = "Parent"
+        ) { 10.dp }
+
+        val child = parentTransition.createChildTransition(label = "child1") { it }.apply {
+            this.animateDp(
+                transitionSpec = { tween(durationMillis = 1000, delayMillis = 100) },
+                label = "Child1"
+            ) { 10.dp }
+        }
+        val grandchild = child.createChildTransition(label = "child1") { it }.apply {
+            this.animateDp(
+                transitionSpec = { tween(durationMillis = 1000, delayMillis = 100) },
+                label = "Grandchild"
+            ) { 10.dp }
+        }
+        grandchild.createChildTransition(label = "child1") { it }.apply {
+            this.animateDp(
+                transitionSpec = { tween(durationMillis = 1000, delayMillis = 100) },
+                label = "GrandGrandchild"
+            ) { 10.dp }
+        }
+        parentTransition.createChildTransition(label = "child2") { it }.apply {
+            this.animateDp(
+                transitionSpec = { tween(durationMillis = 1000, delayMillis = 100) },
+                label = "Child2"
+            ) { 10.dp }
+        }
+    }
+
     //endregion
 
     //region AnimatedContent() animations
@@ -523,6 +589,22 @@
     }
 
     @Test
+    fun animatedContentClockStateAsList() {
+        val search = AnimationSearch.AnimatedContentSearch { }
+        val target = mutableStateOf<IntSize?>(null)
+        rule.searchForAnimation(search) { AnimatedContent(IntSize(10, 10)) { target.value = it } }
+        val clock = TransitionClock(search.animations.first().parseAnimatedContent()!!)
+        rule.runOnIdle {
+            clock.setStateParameters(listOf(20, 30), listOf(40, 50))
+            clock.setClockTime(0)
+        }
+        rule.runOnIdle {
+            assertEquals(TargetState(IntSize(20, 30), IntSize(40, 50)), clock.state)
+            assertEquals(IntSize(40, 50), target.value)
+        }
+    }
+
+    @Test
     fun animatedContentClockProperties() {
         val search = AnimationSearch.AnimatedContentSearch { }
         rule.searchForAnimation(search) { AnimatedContent(1.dp) {} }
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..8f480e6 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
@@ -646,20 +646,19 @@
     }
 
     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
     }
 
     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 +668,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 5b08640..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
 }
@@ -94,10 +98,11 @@
     private fun supportedSearch() = setOf(
         transitionSearch,
         animatedVisibilitySearch,
-    ) + animateXAsStateSearch() + infiniteTransitionSearch()
+    ) + animateXAsStateSearch() + infiniteTransitionSearch() +
+        (if (AnimatedContentComposeAnimation.apiAvailable)
+            setOf(animatedContentSearch) else emptySet())
 
     private fun unsupportedSearch() = if (UnsupportedComposeAnimation.apiAvailable) setOf(
-        animatedContentSearch,
         AnimateContentSizeSearch { clock().trackAnimateContentSize(it) },
         TargetBasedSearch { clock().trackTargetBasedAnimations(it) },
         DecaySearch { clock().trackDecayAnimations(it) }
@@ -200,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/AnimateXAsStateClock.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClock.kt
index bbf646c..7367903 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClock.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClock.kt
@@ -20,16 +20,8 @@
 import androidx.compose.animation.core.TargetBasedAnimation
 import androidx.compose.animation.tooling.ComposeAnimatedProperty
 import androidx.compose.animation.tooling.TransitionInfo
-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.AnimateXAsStateComposeAnimation
 import androidx.compose.ui.tooling.animation.states.TargetState
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
 
 /**
  * [ComposeAnimationClock] for [AnimateXAsStateComposeAnimation].
@@ -57,101 +49,9 @@
 
     private var currAnimation: TargetBasedAnimation<T, V> = getCurrentAnimation()
 
-    @Suppress("UNCHECKED_CAST")
     override fun setStateParameters(par1: Any, par2: Any?) {
-
-        fun parametersAreValid(par1: Any?, par2: Any?): Boolean {
-            return currentValue != null &&
-                par1 != null && par2 != null && par1::class == par2::class
-        }
-
-        fun parametersHasTheSameType(value: Any, par1: Any, par2: Any): Boolean {
-            return value::class == par1::class && value::class == par2::class
-        }
-
-        if (!parametersAreValid(par1, par2)) return
-
-        if (parametersHasTheSameType(currentValue!!, par1, par2!!)) {
-            state = TargetState(par1 as T, par2 as T)
-            return
-        }
-
-        if (par1 is List<*> && par2 is List<*>) {
-            try {
-                state = when (currentValue) {
-                    is IntSize -> TargetState(
-                        IntSize(par1[0] as Int, par1[1] as Int),
-                        IntSize(par2[0] as Int, par2[1] as Int)
-                    )
-
-                    is IntOffset -> TargetState(
-                        IntOffset(par1[0] as Int, par1[1] as Int),
-                        IntOffset(par2[0] as Int, par2[1] as Int)
-                    )
-
-                    is Size -> TargetState(
-                        Size(par1[0] as Float, par1[1] as Float),
-                        Size(par2[0] as Float, par2[1] as Float)
-                    )
-
-                    is Offset -> TargetState(
-                        Offset(par1[0] as Float, par1[1] as Float),
-                        Offset(par2[0] as Float, par2[1] as Float)
-                    )
-
-                    is Rect ->
-                        TargetState(
-                            Rect(
-                                par1[0] as Float,
-                                par1[1] as Float,
-                                par1[2] as Float,
-                                par1[3] as Float
-                            ),
-                            Rect(
-                                par2[0] as Float,
-                                par2[1] as Float,
-                                par2[2] as Float,
-                                par2[3] as Float
-                            ),
-                        )
-                    is Color -> TargetState(
-                        Color(
-                            par1[0] as Float,
-                            par1[1] as Float,
-                            par1[2] as Float,
-                            par1[3] as Float
-                        ),
-                        Color(
-                            par2[0] as Float,
-                            par2[1] as Float,
-                            par2[2] as Float,
-                            par2[3] as Float
-                        ),
-                    )
-
-                    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
-                        )
-                    }
-
-                    else -> {
-                        if (parametersAreValid(par1[0], par2[0]) &&
-                            parametersHasTheSameType(currentValue!!, par1[0]!!, par2[0]!!)
-                        ) TargetState(par1[0], par2[0])
-                        else return
-                    }
-                } as TargetState<T>
-            } catch (_: IndexOutOfBoundsException) {
-                return
-            } catch (_: ClassCastException) {
-                return
-            } catch (_: IllegalArgumentException) {
-                return
-            } catch (_: NullPointerException) {
-                return
-            }
+        parseParametersToValue(currentValue, par1, par2)?.let {
+            state = it
         }
     }
 
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimatedVisibilityClock.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimatedVisibilityClock.kt
index 9f682c7..aeb3e35 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimatedVisibilityClock.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimatedVisibilityClock.kt
@@ -67,7 +67,7 @@
         animation.childTransition?.let { child ->
             return child.allAnimations().map {
                 it.createTransitionInfo(stepMillis)
-            }.sortedBy { it.label }
+            }.sortedBy { it.label }.filter { !IGNORE_TRANSITIONS.contains(it.label) }
         }
         return emptyList()
     }
@@ -76,7 +76,7 @@
         animation.childTransition?.let { child ->
             return child.allAnimations().mapNotNull {
                 ComposeAnimatedProperty(it.label, it.value ?: return@mapNotNull null)
-            }.sortedBy { it.label }
+            }.sortedBy { it.label }.filter { !IGNORE_TRANSITIONS.contains(it.label) }
         }
         return emptyList()
     }
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClock.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClock.kt
index 8149017..da45461 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClock.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/TransitionClock.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.tooling.animation.clock
 
+import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.core.Transition
 import androidx.compose.animation.tooling.ComposeAnimatedProperty
 import androidx.compose.animation.tooling.TransitionInfo
@@ -23,7 +24,7 @@
 import androidx.compose.ui.tooling.animation.states.TargetState
 
 /**
- * [ComposeAnimationClock] for [Transition] animations.
+ * [ComposeAnimationClock] for [Transition] and [AnimatedContent] animations.
  * This clock also controls extension functions such as:
  * * Transition.AnimatedVisibility
  * * Transition.Crossfade
@@ -33,6 +34,7 @@
  *  @sample androidx.compose.animation.samples.AnimatedVisibilityLazyColumnSample
  *  @sample androidx.compose.animation.samples.CrossfadeSample
  *  @sample androidx.compose.animation.samples.TransitionExtensionAnimatedContentSample
+ *  @sample androidx.compose.animation.samples.AnimateIncrementDecrementSample
  */
 internal class TransitionClock<T>(override val animation: TransitionBasedAnimation<T>) :
     ComposeAnimationClock<TransitionBasedAnimation<T>, TargetState<T>> {
@@ -46,9 +48,10 @@
             setClockTime(0)
         }
 
-    @Suppress("UNCHECKED_CAST")
     override fun setStateParameters(par1: Any, par2: Any?) {
-        state = TargetState(par1 as T, par2 as T)
+        parseParametersToValue(state.initial, par1, par2)?.let {
+            state = it
+        }
     }
 
     override fun getAnimatedProperties(): List<ComposeAnimatedProperty> {
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 13dbdcd..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
@@ -29,6 +29,15 @@
 import androidx.compose.animation.core.TweenSpec
 import androidx.compose.animation.core.VectorizedDurationBasedAnimationSpec
 import androidx.compose.animation.tooling.TransitionInfo
+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.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
 
 /** Animations can contain internal only transitions which should be ignored by tooling. */
 internal val IGNORE_TRANSITIONS = listOf("TransformOriginInterruptionHandling")
@@ -89,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()
@@ -143,4 +154,123 @@
         label, animationSpec.javaClass.name,
         startTimeMs, endTimeMs, values
     )
+}
+
+/**
+ * [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!!)) {
+        return TargetState(par1 as T, par2 as T)
+    }
+
+    if (par1 is List<*> && par2 is List<*>) {
+        try {
+            return when (currentValue) {
+                is IntSize -> TargetState(
+                    IntSize(par1[0] as Int, par1[1] as Int),
+                    IntSize(par2[0] as Int, par2[1] as Int)
+                )
+
+                is IntOffset -> TargetState(
+                    IntOffset(par1[0] as Int, par1[1] as Int),
+                    IntOffset(par2[0] as Int, par2[1] as Int)
+                )
+
+                is Size -> TargetState(
+                    Size(par1[0] as Float, par1[1] as Float),
+                    Size(par2[0] as Float, par2[1] as Float)
+                )
+
+                is Offset -> TargetState(
+                    Offset(par1[0] as Float, par1[1] as Float),
+                    Offset(par2[0] as Float, par2[1] as Float)
+                )
+
+                is Rect ->
+                    TargetState(
+                        Rect(
+                            par1[0] as Float,
+                            par1[1] as Float,
+                            par1[2] as Float,
+                            par1[3] as Float
+                        ),
+                        Rect(
+                            par2[0] as Float,
+                            par2[1] as Float,
+                            par2[2] as Float,
+                            par2[3] as Float
+                        ),
+                    )
+
+                is Color -> TargetState(
+                    Color(
+                        par1[0] as Float,
+                        par1[1] as Float,
+                        par1[2] as Float,
+                        par1[3] as Float
+                    ),
+                    Color(
+                        par2[0] as Float,
+                        par2[1] as Float,
+                        par2[2] as Float,
+                        par2[3] as Float
+                    ),
+                )
+
+                is Dp ->
+                    parseDp(par1[0]!!, par2[0]!!)
+
+                else -> {
+                    if (parametersAreValid(par1[0], par2[0]) &&
+                        parametersHasTheSameType(currentValue, par1[0]!!, par2[0]!!)
+                    ) TargetState(par1[0], par2[0])
+                    else return null
+                }
+            } as TargetState<T>
+        } catch (_: IndexOutOfBoundsException) {
+            return null
+        } catch (_: ClassCastException) {
+            return null
+        } catch (_: IllegalArgumentException) {
+            return null
+        } catch (_: NullPointerException) {
+            return null
+        }
+    }
+    return null
 }
\ No newline at end of file
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 f234d88..4eea595 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -300,6 +300,9 @@
     method public static androidx.compose.ui.Modifier onFocusEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusEvent);
   }
 
+  public final class FocusEventModifierNodeKt {
+  }
+
   @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusManager {
     method public void clearFocus(optional boolean force);
     method public boolean moveFocus(int focusDirection);
@@ -382,7 +385,7 @@
     method public static androidx.compose.ui.Modifier focusProperties(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusProperties,kotlin.Unit> scope);
   }
 
-  public final class FocusRequester {
+  @androidx.compose.runtime.Stable public final class FocusRequester {
     ctor public FocusRequester();
     method public boolean captureFocus();
     method public boolean freeFocus();
@@ -407,6 +410,9 @@
     method public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
   }
 
+  public final class FocusRequesterModifierNodeKt {
+  }
+
   public interface FocusState {
     method public boolean getHasFocus();
     method public boolean isCaptured();
@@ -825,17 +831,6 @@
     property public abstract int inputMode;
   }
 
-  public interface ScrollContainerInfo {
-    method public boolean canScrollHorizontally();
-    method public boolean canScrollVertically();
-  }
-
-  public final class ScrollContainerInfoKt {
-    method public static boolean canScroll(androidx.compose.ui.input.ScrollContainerInfo);
-    method public static androidx.compose.ui.Modifier consumeScrollContainerInfo(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.ScrollContainerInfo,kotlin.Unit> consumer);
-    method public static androidx.compose.ui.Modifier provideScrollContainerInfo(androidx.compose.ui.Modifier, androidx.compose.ui.input.ScrollContainerInfo scrollContainerInfo);
-  }
-
 }
 
 package androidx.compose.ui.input.key {
@@ -1766,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);
   }
@@ -2085,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 36fc854..d167cda 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -400,6 +400,9 @@
     method public void onFocusEvent(androidx.compose.ui.focus.FocusState focusState);
   }
 
+  public final class FocusEventModifierNodeKt {
+  }
+
   @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusManager {
     method public void clearFocus(optional boolean force);
     method public boolean moveFocus(int focusDirection);
@@ -492,7 +495,7 @@
     method public void modifyFocusProperties(androidx.compose.ui.focus.FocusProperties focusProperties);
   }
 
-  public final class FocusRequester {
+  @androidx.compose.runtime.Stable public final class FocusRequester {
     ctor public FocusRequester();
     method public boolean captureFocus();
     method public boolean freeFocus();
@@ -537,15 +540,18 @@
   }
 
   public final class FocusRequesterModifierKt {
-    method @androidx.compose.ui.ExperimentalComposeUiApi public static boolean captureFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
     method public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
-    method @androidx.compose.ui.ExperimentalComposeUiApi public static boolean freeFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
-    method @androidx.compose.ui.ExperimentalComposeUiApi public static boolean requestFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
   }
 
   @androidx.compose.ui.ExperimentalComposeUiApi public interface FocusRequesterModifierNode extends androidx.compose.ui.node.DelegatableNode {
   }
 
+  public final class FocusRequesterModifierNodeKt {
+    method @androidx.compose.ui.ExperimentalComposeUiApi public static boolean captureFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
+    method @androidx.compose.ui.ExperimentalComposeUiApi public static boolean freeFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
+    method @androidx.compose.ui.ExperimentalComposeUiApi public static boolean requestFocus(androidx.compose.ui.focus.FocusRequesterModifierNode);
+  }
+
   public interface FocusState {
     method public boolean getHasFocus();
     method public boolean isCaptured();
@@ -972,17 +978,6 @@
     property public abstract int inputMode;
   }
 
-  public interface ScrollContainerInfo {
-    method public boolean canScrollHorizontally();
-    method public boolean canScrollVertically();
-  }
-
-  public final class ScrollContainerInfoKt {
-    method public static boolean canScroll(androidx.compose.ui.input.ScrollContainerInfo);
-    method public static androidx.compose.ui.Modifier consumeScrollContainerInfo(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.ScrollContainerInfo,kotlin.Unit> consumer);
-    method public static androidx.compose.ui.Modifier provideScrollContainerInfo(androidx.compose.ui.Modifier, androidx.compose.ui.input.ScrollContainerInfo scrollContainerInfo);
-  }
-
 }
 
 package androidx.compose.ui.input.key {
@@ -1942,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);
   }
@@ -2276,7 +2280,7 @@
   }
 
   public static fun interface PinnableContainer.PinnedHandle {
-    method public void unpin();
+    method public void release();
   }
 
   public final class PinnableContainerKt {
@@ -2590,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 5fd66d7..6165576 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -300,6 +300,9 @@
     method public static androidx.compose.ui.Modifier onFocusEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusState,kotlin.Unit> onFocusEvent);
   }
 
+  public final class FocusEventModifierNodeKt {
+  }
+
   @kotlin.jvm.JvmDefaultWithCompatibility public interface FocusManager {
     method public void clearFocus(optional boolean force);
     method public boolean moveFocus(int focusDirection);
@@ -382,7 +385,7 @@
     method public static androidx.compose.ui.Modifier focusProperties(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.focus.FocusProperties,kotlin.Unit> scope);
   }
 
-  public final class FocusRequester {
+  @androidx.compose.runtime.Stable public final class FocusRequester {
     ctor public FocusRequester();
     method public boolean captureFocus();
     method public boolean freeFocus();
@@ -407,6 +410,9 @@
     method public static androidx.compose.ui.Modifier focusRequester(androidx.compose.ui.Modifier, androidx.compose.ui.focus.FocusRequester focusRequester);
   }
 
+  public final class FocusRequesterModifierNodeKt {
+  }
+
   public interface FocusState {
     method public boolean getHasFocus();
     method public boolean isCaptured();
@@ -825,17 +831,6 @@
     property public abstract int inputMode;
   }
 
-  public interface ScrollContainerInfo {
-    method public boolean canScrollHorizontally();
-    method public boolean canScrollVertically();
-  }
-
-  public final class ScrollContainerInfoKt {
-    method public static boolean canScroll(androidx.compose.ui.input.ScrollContainerInfo);
-    method public static androidx.compose.ui.Modifier consumeScrollContainerInfo(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.ScrollContainerInfo,kotlin.Unit> consumer);
-    method public static androidx.compose.ui.Modifier provideScrollContainerInfo(androidx.compose.ui.Modifier, androidx.compose.ui.input.ScrollContainerInfo scrollContainerInfo);
-  }
-
 }
 
 package androidx.compose.ui.input.key {
@@ -1766,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);
   }
@@ -2088,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..f9d4bb9 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -78,7 +78,7 @@
         implementation("androidx.savedstate:savedstate-ktx:1.2.0")
         implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
         implementation(project(":lifecycle:lifecycle-runtime"))
-        implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
+        implementation(project(":lifecycle:lifecycle-viewmodel"))
         implementation("androidx.profileinstaller:profileinstaller:1.2.1")
         implementation("androidx.emoji2:emoji2:1.2.0")
 
@@ -175,7 +175,7 @@
                 implementation("androidx.savedstate:savedstate-ktx:1.2.0")
                 implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
                 implementation(project(":lifecycle:lifecycle-runtime"))
-                implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
+                implementation(project(":lifecycle:lifecycle-viewmodel"))
                 implementation("androidx.emoji2:emoji2:1.2.0")
             }
 
@@ -291,6 +291,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/samples/src/main/java/androidx/compose/ui/samples/ScrollableContainerSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ScrollableContainerSample.kt
deleted file mode 100644
index 7146d12..0000000
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ScrollableContainerSample.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.compose.ui.samples
-
-import androidx.annotation.Sampled
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-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.input.canScroll
-import androidx.compose.ui.input.consumeScrollContainerInfo
-import androidx.compose.ui.input.pointer.pointerInput
-import java.util.concurrent.TimeoutException
-import kotlinx.coroutines.withTimeout
-
-@Sampled
-@Composable
-fun ScrollableContainerSample() {
-    var isParentScrollable by remember { mutableStateOf({ false }) }
-
-    Column(Modifier.verticalScroll(rememberScrollState())) {
-
-        Box(modifier = Modifier.consumeScrollContainerInfo {
-            isParentScrollable = { it?.canScroll() == true }
-        }) {
-            Box(Modifier.pointerInput(Unit) {
-                detectTapGestures(
-                    onPress = {
-                        // If there is an ancestor that handles drag events, this press might
-                        // become a drag so delay any work
-                        val doWork = !isParentScrollable() || try {
-                            withTimeout(100) { tryAwaitRelease() }
-                        } catch (e: TimeoutException) {
-                            true
-                        }
-                        if (doWork) println("Do work")
-                    })
-            })
-        }
-    }
-}
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/DrawReorderingTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawReorderingTest.kt
index 568809a..f73781e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawReorderingTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawReorderingTest.kt
@@ -46,6 +46,7 @@
 import org.junit.Assert.assertNotNull
 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
@@ -436,6 +437,7 @@
         )
     }
 
+    @Ignore // b/265025605
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     fun testChangingZOrder() {
@@ -569,6 +571,7 @@
         )
     }
 
+    @Ignore // b/265025605
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     fun testChangingZOrderUncle() {
@@ -628,6 +631,7 @@
         )
     }
 
+    @Ignore // b/265025605
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     fun testChangingReorderedChildSize() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
index 094ea37..905dfcd 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/PainterModifierTest.kt
@@ -94,6 +94,7 @@
 import org.junit.After
 import org.junit.Assert.assertEquals
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -610,6 +611,7 @@
             .assertHeightIsEqualTo(composableHeight)
     }
 
+    @Ignore // b/265030745
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Test
     fun testBitmapPainterScalesContent(): Unit = with(rule.density) {
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/input/nestedscroll/ScrollContainerInfoTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/ScrollContainerInfoTest.kt
deleted file mode 100644
index 4bf39eb..0000000
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/ScrollContainerInfoTest.kt
+++ /dev/null
@@ -1,64 +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.
- */
-
-import androidx.compose.ui.input.ScrollContainerInfo
-import androidx.compose.ui.input.canScroll
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-class ScrollContainerInfoTest {
-
-    @Test
-    fun canScroll_horizontal() {
-        val subject = Subject(horizontal = true)
-
-        assertThat(subject.canScroll()).isTrue()
-    }
-
-    @Test
-    fun canScroll_vertical() {
-        val subject = Subject(vertical = true)
-
-        assertThat(subject.canScroll()).isTrue()
-    }
-
-    @Test
-    fun canScroll_both() {
-        val subject = Subject(horizontal = true, vertical = true)
-
-        assertThat(subject.canScroll()).isTrue()
-    }
-
-    @Test
-    fun canScroll_neither() {
-        val subject = Subject(horizontal = false, vertical = false)
-
-        assertThat(subject.canScroll()).isFalse()
-    }
-
-    class Subject(
-        private val horizontal: Boolean = false,
-        private val vertical: Boolean = false,
-    ) : ScrollContainerInfo {
-        override fun canScrollHorizontally(): Boolean = horizontal
-        override fun canScrollVertically(): Boolean = vertical
-    }
-}
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/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 6bb3fb7..49fc678f 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
@@ -30,7 +30,6 @@
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import android.widget.FrameLayout
-import android.widget.LinearLayout
 import android.widget.RelativeLayout
 import android.widget.TextView
 import androidx.compose.foundation.background
@@ -39,8 +38,6 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.SideEffect
@@ -55,8 +52,6 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.input.canScroll
-import androidx.compose.ui.input.consumeScrollContainerInfo
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.LocalDensity
@@ -66,11 +61,11 @@
 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
@@ -91,7 +86,6 @@
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
-import kotlin.math.roundToInt
 import org.hamcrest.CoreMatchers.endsWith
 import org.hamcrest.CoreMatchers.equalTo
 import org.hamcrest.CoreMatchers.instanceOf
@@ -99,6 +93,7 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import kotlin.math.roundToInt
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -642,19 +637,11 @@
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     fun androidView_noClip() {
         rule.setContent {
-            Box(
-                Modifier
-                    .fillMaxSize()
-                    .background(Color.White)) {
+            Box(Modifier.fillMaxSize().background(Color.White)) {
                 with(LocalDensity.current) {
-                    Box(
-                        Modifier
-                            .requiredSize(150.toDp())
-                            .testTag("box")) {
+                    Box(Modifier.requiredSize(150.toDp()).testTag("box")) {
                         Box(
-                            Modifier
-                                .size(100.toDp(), 100.toDp())
-                                .align(AbsoluteAlignment.TopLeft)
+                            Modifier.size(100.toDp(), 100.toDp()).align(AbsoluteAlignment.TopLeft)
                         ) {
                             AndroidView(factory = { context ->
                                 object : View(context) {
@@ -680,92 +667,6 @@
         }
     }
 
-    @Test
-    fun scrollableViewGroup_propagates_shouldDelay() {
-        val scrollContainerInfo = mutableStateOf({ false })
-        rule.activityRule.scenario.onActivity { activity ->
-            val parentComposeView = ScrollingViewGroup(activity).apply {
-                addView(
-                    ComposeView(activity).apply {
-                        setContent {
-                            Box(modifier = Modifier.consumeScrollContainerInfo {
-                                scrollContainerInfo.value = { it?.canScroll() == true }
-                            })
-                        }
-                    })
-                }
-            activity.setContentView(parentComposeView)
-        }
-
-        rule.runOnIdle {
-            assertThat(scrollContainerInfo.value()).isTrue()
-        }
-    }
-
-    @Test
-    fun nonScrollableViewGroup_doesNotPropagate_shouldDelay() {
-        val scrollContainerInfo = mutableStateOf({ false })
-        rule.activityRule.scenario.onActivity { activity ->
-            val parentComposeView = FrameLayout(activity).apply {
-                addView(
-                    ComposeView(activity).apply {
-                        setContent {
-                            Box(modifier = Modifier.consumeScrollContainerInfo {
-                                scrollContainerInfo.value = { it?.canScroll() == true }
-                            })
-                        }
-                    })
-            }
-            activity.setContentView(parentComposeView)
-        }
-
-        rule.runOnIdle {
-            assertThat(scrollContainerInfo.value()).isFalse()
-        }
-    }
-
-    @Test
-    fun viewGroup_propagates_shouldDelayTrue() {
-        lateinit var layout: View
-        rule.setContent {
-            Column(Modifier.verticalScroll(rememberScrollState())) {
-                AndroidView(
-                    factory = {
-                        layout = LinearLayout(it)
-                        layout
-                    }
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            // View#isInScrollingContainer is hidden, check the parent manually.
-            val shouldDelay = (layout.parent as ViewGroup).shouldDelayChildPressedState()
-            assertThat(shouldDelay).isTrue()
-        }
-    }
-
-    @Test
-    fun viewGroup_propagates_shouldDelayFalse() {
-        lateinit var layout: View
-        rule.setContent {
-            Column {
-                AndroidView(
-                    factory = {
-                        layout = LinearLayout(it)
-                        layout
-                    }
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            // View#isInScrollingContainer is hidden, check the parent manually.
-            val shouldDelay = (layout.parent as ViewGroup).shouldDelayChildPressedState()
-            assertThat(shouldDelay).isFalse()
-        }
-    }
-
     private class StateSavingView(
         private val key: String,
         private val value: String,
@@ -797,8 +698,4 @@
             value,
             displayMetrics
         ).roundToInt()
-
-    class ScrollingViewGroup(context: Context) : FrameLayout(context) {
-        override fun shouldDelayChildPressedState() = true
-    }
 }
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 65fe4f0..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
@@ -73,8 +73,6 @@
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SmallTest
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 import org.hamcrest.CoreMatchers.instanceOf
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
@@ -85,6 +83,8 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
 import kotlin.math.roundToInt
 
 @MediumTest
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 f50648d..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
@@ -86,8 +86,6 @@
 import androidx.compose.ui.input.InputMode.Companion.Touch
 import androidx.compose.ui.input.InputModeManager
 import androidx.compose.ui.input.InputModeManagerImpl
-import androidx.compose.ui.input.ModifierLocalScrollContainerInfo
-import androidx.compose.ui.input.ScrollContainerInfo
 import androidx.compose.ui.input.key.Key.Companion.Back
 import androidx.compose.ui.input.key.Key.Companion.DirectionCenter
 import androidx.compose.ui.input.key.Key.Companion.DirectionDown
@@ -117,7 +115,6 @@
 import androidx.compose.ui.input.rotary.onRotaryScrollEvent
 import androidx.compose.ui.layout.RootMeasurePolicy
 import androidx.compose.ui.modifier.ModifierLocalManager
-import androidx.compose.ui.modifier.ModifierLocalProvider
 import androidx.compose.ui.node.InternalCoreApi
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.LayoutNode.UsageByParent
@@ -216,34 +213,6 @@
         false
     }
 
-    // We don't have a way to determine direction in Android, return true for both directions.
-    private val scrollContainerInfo = object : ModifierLocalProvider<ScrollContainerInfo?> {
-        override val key = ModifierLocalScrollContainerInfo
-        override val value = object : ScrollContainerInfo {
-            // Intentionally not using [View#canScrollHorizontally], to maintain semantics of
-            // View#isInScrollingContainer
-            override fun canScrollHorizontally(): Boolean =
-                view.isInScrollableViewGroup()
-
-            // Intentionally not using [View#canScrollVertically], to maintain semantics of
-            // View#isInScrollingContainer
-            override fun canScrollVertically(): Boolean =
-                view.isInScrollableViewGroup()
-
-            // Copied from View#isInScrollingContainer() which is @hide
-            private fun View.isInScrollableViewGroup(): Boolean {
-                var p = parent
-                while (p != null && p is ViewGroup) {
-                    if (p.shouldDelayChildPressedState()) {
-                        return true
-                    }
-                    p = p.parent
-                }
-                return false
-            }
-        }
-    }
-
     private val canvasHolder = CanvasHolder()
 
     override val root = LayoutNode().also {
@@ -255,7 +224,6 @@
             .then(rotaryInputModifier)
             .then(focusOwner.modifier)
             .then(keyInputModifier)
-            .then(scrollContainerInfo)
     }
 
     override val rootForTest: RootForTest = this
@@ -766,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) {
@@ -789,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/viewinterop/AndroidViewHolder.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
index 285c1ce..4cf957db 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
@@ -35,8 +35,6 @@
 import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
 import androidx.compose.ui.input.pointer.pointerInteropFilter
-import androidx.compose.ui.input.canScroll
-import androidx.compose.ui.input.consumeScrollContainerInfo
 import androidx.compose.ui.layout.IntrinsicMeasurable
 import androidx.compose.ui.layout.IntrinsicMeasureScope
 import androidx.compose.ui.layout.Measurable
@@ -183,8 +181,6 @@
     private val nestedScrollingParentHelper: NestedScrollingParentHelper =
         NestedScrollingParentHelper(this)
 
-    private var isInScrollContainer: () -> Boolean = { true }
-
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
         view?.measure(widthMeasureSpec, heightMeasureSpec)
         setMeasuredDimension(view?.measuredWidth ?: 0, view?.measuredHeight ?: 0)
@@ -284,15 +280,11 @@
                     (layoutNode.owner as? AndroidComposeView)
                         ?.drawAndroidView(this@AndroidViewHolder, canvas.nativeCanvas)
                 }
-            }
-            .onGloballyPositioned {
+            }.onGloballyPositioned {
                 // The global position of this LayoutNode can change with it being replaced. For
                 // these cases, we need to inform the View.
                 layoutAccordingTo(layoutNode)
             }
-            .consumeScrollContainerInfo { scrollContainerInfo ->
-                isInScrollContainer = { scrollContainerInfo?.canScroll() == true }
-            }
         layoutNode.modifier = modifier.then(coreModifier)
         onModifierChanged = { layoutNode.modifier = it.then(coreModifier) }
 
@@ -406,7 +398,9 @@
         }
     }
 
-    override fun shouldDelayChildPressedState(): Boolean = isInScrollContainer()
+    // TODO: b/203141462 - consume whether the AndroidView() is inside a scrollable container, and
+    //  use that to set this. In the meantime set true as the defensive default.
+    override fun shouldDelayChildPressedState(): Boolean = true
 
     // NestedScrollingParent3
     override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
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/draw/DrawModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
index ee54cb1..01c13da 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
@@ -17,18 +17,19 @@
 package androidx.compose.ui.draw
 
 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.Size
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.graphics.drawscope.DrawScope
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.modifierElementOf
 
 /**
  * A [Modifier.Element] that draws into the space of the layout.
@@ -83,38 +84,35 @@
 /**
  * Draw into a [Canvas] behind the modified content.
  */
+@OptIn(ExperimentalComposeUiApi::class)
 fun Modifier.drawBehind(
     onDraw: DrawScope.() -> Unit
 ) = this.then(
-    DrawBackgroundModifier(
-        onDraw = onDraw,
-        inspectorInfo = debugInspectorInfo {
+    modifierElementOf(
+        key = onDraw,
+        create = {
+            DrawBackgroundModifier(onDraw = onDraw)
+        },
+        update = {
+            it.onDraw = onDraw
+        },
+        definitions = debugInspectorInfo {
             name = "drawBehind"
             properties["onDraw"] = onDraw
         }
     )
+
 )
 
+@OptIn(ExperimentalComposeUiApi::class)
 private class DrawBackgroundModifier(
-    val onDraw: DrawScope.() -> Unit,
-    inspectorInfo: InspectorInfo.() -> Unit
-) : DrawModifier, InspectorValueInfo(inspectorInfo) {
+    var onDraw: DrawScope.() -> Unit
+) : Modifier.Node(), DrawModifierNode {
 
     override fun ContentDrawScope.draw() {
         onDraw()
         drawContent()
     }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is DrawBackgroundModifier) return false
-
-        return onDraw == other.onDraw
-    }
-
-    override fun hashCode(): Int {
-        return onDraw.hashCode()
-    }
 }
 
 /**
@@ -245,35 +243,34 @@
  * Creates a [DrawModifier] that allows the developer to draw before or after the layout's
  * contents. It also allows the modifier to adjust the layout's canvas.
  */
+@OptIn(ExperimentalComposeUiApi::class)
 fun Modifier.drawWithContent(
     onDraw: ContentDrawScope.() -> Unit
 ): Modifier = this.then(
-    DrawWithContentModifier(
-        onDraw = onDraw,
-        inspectorInfo = debugInspectorInfo {
+    modifierElementOf(
+        key = onDraw,
+        create = {
+            DrawWithContentModifier(
+                onDraw = onDraw
+            )
+        },
+        update = {
+            it.onDraw = onDraw
+        },
+        definitions = debugInspectorInfo {
             name = "drawWithContent"
             properties["onDraw"] = onDraw
         }
     )
+
 )
 
+@OptIn(ExperimentalComposeUiApi::class)
 private class DrawWithContentModifier(
-    val onDraw: ContentDrawScope.() -> Unit,
-    inspectorInfo: InspectorInfo.() -> Unit
-) : DrawModifier, InspectorValueInfo(inspectorInfo) {
+    var onDraw: ContentDrawScope.() -> Unit
+) : Modifier.Node(), DrawModifierNode {
 
     override fun ContentDrawScope.draw() {
         onDraw()
     }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is DrawWithContentModifier) return false
-
-        return onDraw == other.onDraw
-    }
-
-    override fun hashCode(): Int {
-        return onDraw.hashCode()
-    }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusChangedModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusChangedModifier.kt
index 54ee8d3..5c4c835 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusChangedModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusChangedModifier.kt
@@ -48,7 +48,7 @@
 private class FocusChangedModifierNode(
     var onFocusChanged: (FocusState) -> Unit
 ) : FocusEventModifierNode, Modifier.Node() {
-    var focusState: FocusState? = null
+    private var focusState: FocusState? = null
     override fun onFocusEvent(focusState: FocusState) {
         if (this.focusState != focusState) {
             this.focusState = focusState
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusDirection.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusDirection.kt
new file mode 100644
index 0000000..3167c26
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusDirection.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+
+/**
+ * The [FocusDirection] is used to specify the direction for a [FocusManager.moveFocus]
+ * request.
+ *
+ * @sample androidx.compose.ui.samples.MoveFocusSample
+ */
+@kotlin.jvm.JvmInline
+value class FocusDirection internal constructor(@Suppress("unused") private val value: Int) {
+
+    override fun toString(): String {
+        return when (this) {
+            Next -> "Next"
+            Previous -> "Previous"
+            Left -> "Left"
+            Right -> "Right"
+            Up -> "Up"
+            Down -> "Down"
+            @OptIn(ExperimentalComposeUiApi::class)
+            Enter -> "Enter"
+            @OptIn(ExperimentalComposeUiApi::class)
+            Exit -> "Exit"
+            else -> "Invalid FocusDirection"
+        }
+    }
+
+    companion object {
+        /**
+         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
+         *  next focusable item.
+         *
+         *  @sample androidx.compose.ui.samples.MoveFocusSample
+         */
+        val Next: FocusDirection = FocusDirection(1)
+
+        /**
+         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
+         *  previous focusable item.
+         *
+         *  @sample androidx.compose.ui.samples.MoveFocusSample
+         */
+        val Previous: FocusDirection = FocusDirection(2)
+
+        /**
+         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
+         *  next focusable item to the left of the currently focused item.
+         *
+         *  @sample androidx.compose.ui.samples.MoveFocusSample
+         */
+        val Left: FocusDirection = FocusDirection(3)
+
+        /**
+         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
+         *  next focusable item to the right of the currently focused item.
+         *
+         *  @sample androidx.compose.ui.samples.MoveFocusSample
+         */
+        val Right: FocusDirection = FocusDirection(4)
+
+        /**
+         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
+         *  next focusable item that is above the currently focused item.
+         *
+         *  @sample androidx.compose.ui.samples.MoveFocusSample
+         */
+        val Up: FocusDirection = FocusDirection(5)
+
+        /**
+         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
+         *  next focusable item that is below the currently focused item.
+         *
+         *  @sample androidx.compose.ui.samples.MoveFocusSample
+         */
+        val Down: FocusDirection = FocusDirection(6)
+
+        /**
+         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
+         *  next focusable item that is a child of the currently focused item.
+         */
+        @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+        @get:ExperimentalComposeUiApi
+        @ExperimentalComposeUiApi
+        val Enter: FocusDirection = FocusDirection(7)
+
+        /**
+         *  Direction used in [FocusManager.moveFocus] to indicate that you want to move focus to
+         *  the parent of the currently focused item.
+         */
+        @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+        @get:ExperimentalComposeUiApi
+        @ExperimentalComposeUiApi
+        val Exit: FocusDirection = FocusDirection(8)
+
+        /**
+         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
+         *  next focusable item that is a child of the currently focused item.
+         */
+        @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET", "Unused")
+        @get:ExperimentalComposeUiApi
+        @ExperimentalComposeUiApi
+        @Deprecated(
+            "Use FocusDirection.Enter instead.",
+            ReplaceWith("Enter", "androidx.compose.ui.focus.FocusDirection.Companion.Enter"),
+            DeprecationLevel.WARNING
+        )
+        val In: FocusDirection = Enter
+
+        /**
+         *  Direction used in [FocusManager.moveFocus] to indicate that you want to move focus to
+         *  the parent of the currently focused item.
+         */
+        @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET", "Unused")
+        @get:ExperimentalComposeUiApi
+        @ExperimentalComposeUiApi
+        @Deprecated(
+            "Use FocusDirection.Exit instead.",
+            ReplaceWith("Exit", "androidx.compose.ui.focus.FocusDirection.Companion.Exit"),
+            DeprecationLevel.WARNING
+        )
+        val Out: FocusDirection = Exit
+    }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifier.kt
index 4b175e1..0b99f2a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifier.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.
@@ -18,30 +18,7 @@
 
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusStateImpl.Active
-import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
-import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Inactive
-import androidx.compose.ui.internal.JvmDefaultWithCompatibility
-import androidx.compose.ui.node.DelegatableNode
-import androidx.compose.ui.node.Nodes
 import androidx.compose.ui.node.modifierElementOf
-import androidx.compose.ui.node.visitAncestors
-import androidx.compose.ui.node.visitChildren
-
-/**
- * Implement this interface create a modifier node that can be used to observe focus state changes
- * to a [FocusTargetModifierNode] down the hierarchy.
- */
-@ExperimentalComposeUiApi
-interface FocusEventModifierNode : DelegatableNode {
-
-    /**
-     * A parent FocusEventNode is notified of [FocusState] changes to the [FocusTargetModifierNode]
-     * associated with this [FocusEventModifierNode].
-     */
-    fun onFocusEvent(focusState: FocusState)
-}
 
 /**
  * A [modifier][Modifier.Element] that can be used to observe focus state events.
@@ -55,37 +32,13 @@
     fun onFocusEvent(focusState: FocusState)
 }
 
-@OptIn(ExperimentalComposeUiApi::class)
-internal class FocusEventModifierNodeImpl(
-    var onFocusEvent: (FocusState) -> Unit
-) : FocusEventModifierNode, Modifier.Node() {
-
-    override fun onFocusEvent(focusState: FocusState) {
-        this.onFocusEvent.invoke(focusState)
-    }
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
-internal fun FocusEventModifierNode.getFocusState(): FocusState {
-    visitChildren(Nodes.FocusTarget) {
-        when (val focusState = it.focusStateImpl) {
-            // If we find a focused child, we use that child's state as the aggregated state.
-            Active, ActiveParent, Captured -> return focusState
-            // We use the Inactive state only if we don't have a focused child.
-            // ie. we ignore this child if another child provides aggregated state.
-            Inactive -> return@visitChildren
-        }
-    }
-    return Inactive
-}
-
 /**
  * Add this modifier to a component to observe focus state events.
  */
 @Suppress("ModifierInspectorInfo") // b/251831790.
 fun Modifier.onFocusEvent(onFocusEvent: (FocusState) -> Unit): Modifier = this.then(
     @OptIn(ExperimentalComposeUiApi::class)
-    modifierElementOf(
+    (modifierElementOf(
         key = onFocusEvent,
         create = { FocusEventModifierNodeImpl(onFocusEvent) },
         update = { it.onFocusEvent = onFocusEvent },
@@ -93,24 +46,15 @@
             name = "onFocusEvent"
             properties["onFocusEvent"] = onFocusEvent
         }
-    )
+    ))
 )
 
-/**
- * Sends a "Focus Event" up the hierarchy that asks all [FocusEventModifierNode]s to recompute their
- * observed focus state.
- *
- * Make this public after [FocusTargetModifierNode] is made public.
- */
-@ExperimentalComposeUiApi
-internal fun FocusTargetModifierNode.refreshFocusEventNodes() {
-    visitAncestors(Nodes.FocusEvent or Nodes.FocusTarget) {
-        // If we reach the previous focus target node, we have gone too far, as
-        //  this is applies to the another focus event.
-        if (it.isKind(Nodes.FocusTarget)) return
+@OptIn(ExperimentalComposeUiApi::class)
+private class FocusEventModifierNodeImpl(
+    var onFocusEvent: (FocusState) -> Unit
+) : FocusEventModifierNode, Modifier.Node() {
 
-        // TODO(251833873): Consider caching it.getFocusState().
-        check(it is FocusEventModifierNode)
-        it.onFocusEvent(it.getFocusState())
+    override fun onFocusEvent(focusState: FocusState) {
+        this.onFocusEvent.invoke(focusState)
     }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifierNode.kt
new file mode 100644
index 0000000..f1d9d4a
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusEventModifierNode.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.focus.FocusStateImpl.Active
+import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
+import androidx.compose.ui.focus.FocusStateImpl.Captured
+import androidx.compose.ui.focus.FocusStateImpl.Inactive
+import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.Nodes
+import androidx.compose.ui.node.visitAncestors
+import androidx.compose.ui.node.visitChildren
+
+/**
+ * Implement this interface create a modifier node that can be used to observe focus state changes
+ * to a [FocusTargetModifierNode] down the hierarchy.
+ */
+@ExperimentalComposeUiApi
+interface FocusEventModifierNode : DelegatableNode {
+
+    /**
+     * A parent FocusEventNode is notified of [FocusState] changes to the [FocusTargetModifierNode]
+     * associated with this [FocusEventModifierNode].
+     */
+    fun onFocusEvent(focusState: FocusState)
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun FocusEventModifierNode.getFocusState(): FocusState {
+    visitChildren(Nodes.FocusTarget) {
+        when (val focusState = it.focusStateImpl) {
+            // If we find a focused child, we use that child's state as the aggregated state.
+            Active, ActiveParent, Captured -> return focusState
+            // We use the Inactive state only if we don't have a focused child.
+            // ie. we ignore this child if another child provides aggregated state.
+            Inactive -> return@visitChildren
+        }
+    }
+    return Inactive
+}
+
+/**
+ * Sends a "Focus Event" up the hierarchy that asks all [FocusEventModifierNode]s to recompute their
+ * observed focus state.
+ *
+ * Make this public after [FocusTargetModifierNode] is made public.
+ */
+@ExperimentalComposeUiApi
+internal fun FocusTargetModifierNode.refreshFocusEventNodes() {
+    visitAncestors(Nodes.FocusEvent or Nodes.FocusTarget) {
+        // If we reach the previous focus target node, we have gone too far, as
+        //  this is applies to the another focus event.
+        if (it.isKind(Nodes.FocusTarget)) return
+
+        // TODO(251833873): Consider caching it.getFocusState().
+        check(it is FocusEventModifierNode)
+        it.onFocusEvent(it.getFocusState())
+    }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
index 7176b18..97c3657 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
@@ -16,31 +16,7 @@
 
 package androidx.compose.ui.focus
 
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusDirection.Companion.Next
-import androidx.compose.ui.focus.FocusDirection.Companion.Previous
-import androidx.compose.ui.focus.FocusRequester.Companion.Cancel
-import androidx.compose.ui.focus.FocusRequester.Companion.Default
-import androidx.compose.ui.focus.FocusStateImpl.Active
-import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
-import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Inactive
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.KeyInputModifierNode
-import androidx.compose.ui.input.rotary.RotaryScrollEvent
 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
-import androidx.compose.ui.node.DelegatableNode
-import androidx.compose.ui.node.NodeKind
-import androidx.compose.ui.node.Nodes
-import androidx.compose.ui.node.ancestors
-import androidx.compose.ui.node.modifierElementOf
-import androidx.compose.ui.node.nearestAncestor
-import androidx.compose.ui.node.visitLocalChildren
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastForEachReversed
 
 @JvmDefaultWithCompatibility
 interface FocusManager {
@@ -67,226 +43,3 @@
      */
     fun moveFocus(focusDirection: FocusDirection): Boolean
 }
-
-/**
- * The focus manager is used by different [Owner][androidx.compose.ui.node.Owner] implementations
- * to control focus.
- */
-internal class FocusOwnerImpl(onRequestApplyChangesListener: (() -> Unit) -> Unit) : FocusOwner {
-
-    @OptIn(ExperimentalComposeUiApi::class)
-    internal var rootFocusNode = FocusTargetModifierNode()
-
-    private val focusInvalidationManager = FocusInvalidationManager(onRequestApplyChangesListener)
-
-    /**
-     * A [Modifier] that can be added to the [Owners][androidx.compose.ui.node.Owner] modifier
-     * list that contains the modifiers required by the focus system. (Eg, a root focus modifier).
-     */
-    // TODO(b/168831247): return an empty Modifier when there are no focusable children.
-    @Suppress("ModifierInspectorInfo") // b/251831790.
-    @OptIn(ExperimentalComposeUiApi::class)
-    override val modifier: Modifier = modifierElementOf(
-        create = { rootFocusNode },
-        definitions = { name = "RootFocusTarget" }
-    )
-
-    override lateinit var layoutDirection: LayoutDirection
-
-    /**
-     * The [Owner][androidx.compose.ui.node.Owner] calls this function when it gains focus. This
-     * informs the [focus manager][FocusOwnerImpl] that the
-     * [Owner][androidx.compose.ui.node.Owner] gained focus, and that it should propagate this
-     * focus to one of the focus modifiers in the component hierarchy.
-     */
-    override fun takeFocus() {
-        // If the focus state is not Inactive, it indicates that the focus state is already
-        // set (possibly by dispatchWindowFocusChanged). So we don't update the state.
-        @OptIn(ExperimentalComposeUiApi::class)
-        if (rootFocusNode.focusStateImpl == Inactive) {
-            rootFocusNode.focusStateImpl = Active
-            // TODO(b/152535715): propagate focus to children based on child focusability.
-            //  moveFocus(FocusDirection.Enter)
-        }
-    }
-
-    /**
-     * The [Owner][androidx.compose.ui.node.Owner] calls this function when it loses focus. This
-     * informs the [focus manager][FocusOwnerImpl] that the
-     * [Owner][androidx.compose.ui.node.Owner] lost focus, and that it should clear focus from
-     * all the focus modifiers in the component hierarchy.
-     */
-    override fun releaseFocus() {
-        @OptIn(ExperimentalComposeUiApi::class)
-        rootFocusNode.clearFocus(forced = true, refreshFocusEvents = true)
-    }
-
-    /**
-     * Call this function to set the focus to the root focus modifier.
-     *
-     * @param force: Whether we should forcefully clear focus regardless of whether we have
-     * any components that have captured focus.
-     *
-     * This could be used to clear focus when a user clicks on empty space outside a focusable
-     * component.
-     */
-    override fun clearFocus(force: Boolean) {
-        clearFocus(force, refreshFocusEvents = true)
-    }
-
-    @OptIn(ExperimentalComposeUiApi::class)
-    override fun clearFocus(force: Boolean, refreshFocusEvents: Boolean) {
-        // If this hierarchy had focus before clearing it, it indicates that the host view has
-        // focus. So after clearing focus within the compose hierarchy, we should restore focus to
-        // the root focus modifier to maintain consistency with the host view.
-        val rootInitialState = rootFocusNode.focusStateImpl
-        if (rootFocusNode.clearFocus(force, refreshFocusEvents)) {
-            rootFocusNode.focusStateImpl = when (rootInitialState) {
-                Active, ActiveParent, Captured -> Active
-                Inactive -> Inactive
-            }
-        }
-    }
-
-    /**
-     * Moves focus in the specified direction.
-     *
-     * @return true if focus was moved successfully. false if the focused item is unchanged.
-     */
-    @OptIn(ExperimentalComposeUiApi::class)
-    override fun moveFocus(focusDirection: FocusDirection): Boolean {
-
-        // If there is no active node in this sub-hierarchy, we can't move focus.
-        val source = rootFocusNode.findActiveFocusNode() ?: return false
-
-        // Check if a custom focus traversal order is specified.
-        when (val next = source.customFocusSearch(focusDirection, layoutDirection)) {
-            @OptIn(ExperimentalComposeUiApi::class)
-            Cancel -> return false
-            Default -> {
-                val foundNextItem =
-                    rootFocusNode.focusSearch(focusDirection, layoutDirection) { destination ->
-                        if (destination == source) return@focusSearch false
-                        checkNotNull(destination.nearestAncestor(Nodes.FocusTarget)) {
-                            "Focus search landed at the root."
-                        }
-                        // If we found a potential next item, move focus to it.
-                        destination.requestFocus()
-                    }
-                // If we didn't find a potential next item, try to wrap around.
-                return foundNextItem || wrapAroundFocus(focusDirection)
-            }
-            else -> return next.findFocusTarget { it.requestFocus() } ?: false
-        }
-    }
-
-    /**
-     * Dispatches a key event through the compose hierarchy.
-     */
-    @OptIn(ExperimentalComposeUiApi::class)
-    override fun dispatchKeyEvent(keyEvent: KeyEvent): Boolean {
-        val activeFocusTarget = rootFocusNode.findActiveFocusNode()
-        checkNotNull(activeFocusTarget) {
-            "Event can't be processed because we do not have an active focus target."
-        }
-        val focusedKeyInputNode = activeFocusTarget.lastLocalKeyInputNode()
-            ?: activeFocusTarget.nearestAncestor(Nodes.KeyInput)
-
-        focusedKeyInputNode?.traverseAncestors(
-            type = Nodes.KeyInput,
-            onPreVisit = { if (it.onPreKeyEvent(keyEvent)) return true },
-            onVisit = { if (it.onKeyEvent(keyEvent)) return true }
-        )
-
-        return false
-    }
-
-    /**
-     * Dispatches a rotary scroll event through the compose hierarchy.
-     */
-    @OptIn(ExperimentalComposeUiApi::class)
-    override fun dispatchRotaryEvent(event: RotaryScrollEvent): Boolean {
-        val focusedRotaryInputNode = rootFocusNode.findActiveFocusNode()
-            ?.nearestAncestor(Nodes.RotaryInput)
-
-        focusedRotaryInputNode?.traverseAncestors(
-            type = Nodes.RotaryInput,
-            onPreVisit = { if (it.onPreRotaryScrollEvent(event)) return true },
-            onVisit = { if (it.onRotaryScrollEvent(event)) return true }
-        )
-
-        return false
-    }
-
-    @OptIn(ExperimentalComposeUiApi::class)
-    override fun scheduleInvalidation(node: FocusTargetModifierNode) {
-        focusInvalidationManager.scheduleInvalidation(node)
-    }
-
-    @OptIn(ExperimentalComposeUiApi::class)
-    override fun scheduleInvalidation(node: FocusEventModifierNode) {
-        focusInvalidationManager.scheduleInvalidation(node)
-    }
-
-    @OptIn(ExperimentalComposeUiApi::class)
-    override fun scheduleInvalidation(node: FocusPropertiesModifierNode) {
-        focusInvalidationManager.scheduleInvalidation(node)
-    }
-
-    @ExperimentalComposeUiApi
-    private inline fun <reified T : DelegatableNode> T.traverseAncestors(
-        type: NodeKind<T>,
-        onPreVisit: (T) -> Unit,
-        onVisit: (T) -> Unit
-    ) {
-        val ancestors = ancestors(type)
-        ancestors?.fastForEachReversed(onPreVisit)
-        onPreVisit(this)
-        onVisit(this)
-        ancestors?.fastForEach(onVisit)
-    }
-
-    /**
-     * Searches for the currently focused item, and returns its coordinates as a rect.
-     */
-    override fun getFocusRect(): Rect? {
-        @OptIn(ExperimentalComposeUiApi::class)
-        return rootFocusNode.findActiveFocusNode()?.focusRect()
-    }
-
-    @OptIn(ExperimentalComposeUiApi::class)
-    private fun DelegatableNode.lastLocalKeyInputNode(): KeyInputModifierNode? {
-        var focusedKeyInputNode: KeyInputModifierNode? = null
-        visitLocalChildren(Nodes.FocusTarget or Nodes.KeyInput) { modifierNode ->
-            if (modifierNode.isKind(Nodes.FocusTarget)) return focusedKeyInputNode
-
-            check(modifierNode is KeyInputModifierNode)
-            focusedKeyInputNode = modifierNode
-        }
-        return focusedKeyInputNode
-    }
-
-    // TODO(b/144116848): This is a hack to make Next/Previous wrap around. This must be
-    //  replaced by code that sends the move request back to the view system. The view system
-    //  will then pass focus to other views, and ultimately return back to this compose view.
-    private fun wrapAroundFocus(focusDirection: FocusDirection): Boolean {
-        // Wrap is not supported when this sub-hierarchy doesn't have focus.
-        @OptIn(ExperimentalComposeUiApi::class)
-        if (!rootFocusNode.focusState.hasFocus || rootFocusNode.focusState.isFocused) return false
-
-        // Next and Previous wraps around.
-        when (focusDirection) {
-            Next, Previous -> {
-                // Clear Focus to send focus the root node.
-                clearFocus(force = false)
-                @OptIn(ExperimentalComposeUiApi::class)
-                if (!rootFocusNode.focusState.isFocused) return false
-
-                // Wrap around by calling moveFocus after the root gains focus.
-                return moveFocus(focusDirection)
-            }
-            // We only wrap-around for 1D Focus search.
-            else -> return false
-        }
-    }
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusModifier.kt
index cf58238..a6aabd1 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusModifier.kt
@@ -18,108 +18,6 @@
 
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusStateImpl.Active
-import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
-import androidx.compose.ui.focus.FocusStateImpl.Captured
-import androidx.compose.ui.focus.FocusStateImpl.Inactive
-import androidx.compose.ui.layout.BeyondBoundsLayout
-import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
-import androidx.compose.ui.modifier.ModifierLocalNode
-import androidx.compose.ui.node.Nodes
-import androidx.compose.ui.node.ObserverNode
-import androidx.compose.ui.node.modifierElementOf
-import androidx.compose.ui.node.observeReads
-import androidx.compose.ui.node.requireOwner
-import androidx.compose.ui.node.visitAncestors
-
-/**
- * This modifier node can be used to create a modifier that makes a component focusable.
- * Use a different instance of [FocusTargetModifierNode] for each focusable component.
- */
-@ExperimentalComposeUiApi
-class FocusTargetModifierNode : ObserverNode, ModifierLocalNode, Modifier.Node() {
-    /**
-     * The [FocusState] associated with this [FocusTargetModifierNode].
-     */
-    val focusState: FocusState
-        get() = focusStateImpl
-
-    internal var focusStateImpl = Inactive
-    internal val beyondBoundsLayoutParent: BeyondBoundsLayout?
-        get() = ModifierLocalBeyondBoundsLayout.current
-
-    override fun onObservedReadsChanged() {
-        val previousFocusState = focusState
-        invalidateFocus()
-        if (previousFocusState != focusState) refreshFocusEventNodes()
-    }
-
-    internal fun onRemoved() {
-        when (focusState) {
-            // Clear focus from the current FocusTarget.
-            // This currently clears focus from the entire hierarchy, but we can change the
-            // implementation so that focus is sent to the immediate focus parent.
-            Active, Captured -> requireOwner().focusOwner.clearFocus(force = true)
-
-            ActiveParent, Inactive -> scheduleInvalidationForFocusEvents()
-        }
-    }
-
-    internal fun invalidateFocus() {
-        when (focusState) {
-            // Clear focus from the current FocusTarget.
-            // This currently clears focus from the entire hierarchy, but we can change the
-            // implementation so that focus is sent to the immediate focus parent.
-            Active, Captured -> {
-                lateinit var focusProperties: FocusProperties
-                observeReads {
-                    focusProperties = fetchFocusProperties()
-                }
-                if (!focusProperties.canFocus) {
-                    requireOwner().focusOwner.clearFocus(force = true)
-                }
-            }
-
-            ActiveParent, Inactive -> {}
-        }
-    }
-
-    /**
-     * Visits parent [FocusPropertiesModifierNode]s and runs
-     * [FocusPropertiesModifierNode.modifyFocusProperties] on each parent.
-     * This effectively collects an aggregated focus state.
-     */
-    @ExperimentalComposeUiApi
-    internal fun fetchFocusProperties(): FocusProperties {
-        val properties = FocusPropertiesImpl()
-        visitAncestors(Nodes.FocusProperties or Nodes.FocusTarget) {
-            // If we reach the previous default focus properties node, we have gone too far, as
-            //  this is applies to the parent focus modifier.
-            if (it.isKind(Nodes.FocusTarget)) return properties
-
-            // Parent can override any values set by this
-            check(it is FocusPropertiesModifierNode)
-            it.modifyFocusProperties(properties)
-        }
-        return properties
-    }
-
-    internal fun scheduleInvalidationForFocusEvents() {
-        visitAncestors(Nodes.FocusEvent or Nodes.FocusTarget) {
-            if (it.isKind(Nodes.FocusTarget)) return@visitAncestors
-
-            check(it is FocusEventModifierNode)
-            requireOwner().focusOwner.scheduleInvalidation(it)
-        }
-    }
-
-    internal companion object {
-        internal val FocusTargetModifierElement = modifierElementOf(
-            create = { FocusTargetModifierNode() },
-            definitions = { name = "focusTarget" }
-        )
-    }
-}
 
 /**
  * Add this modifier to a component to make it focusable.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOrderModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOrderModifier.kt
index d82bd8e..28afe5f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOrderModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOrderModifier.kt
@@ -16,20 +16,8 @@
 
 package androidx.compose.ui.focus
 
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusDirection.Companion.Down
-import androidx.compose.ui.focus.FocusDirection.Companion.Enter
-import androidx.compose.ui.focus.FocusDirection.Companion.Exit
-import androidx.compose.ui.focus.FocusDirection.Companion.Left
-import androidx.compose.ui.focus.FocusDirection.Companion.Next
-import androidx.compose.ui.focus.FocusDirection.Companion.Previous
-import androidx.compose.ui.focus.FocusDirection.Companion.Right
-import androidx.compose.ui.focus.FocusDirection.Companion.Up
-import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
-import androidx.compose.ui.unit.LayoutDirection.Ltr
-import androidx.compose.ui.unit.LayoutDirection.Rtl
 
 /**
  * A [modifier][Modifier.Element] that can be used to set a custom focus traversal order.
@@ -56,6 +44,7 @@
  */
 @Deprecated("Use FocusProperties instead")
 class FocusOrder internal constructor(private val focusProperties: FocusProperties) {
+    @Suppress("unused")
     constructor() : this(FocusPropertiesImpl())
 
     /**
@@ -203,53 +192,6 @@
     .focusRequester(focusRequester)
     .focusProperties(FocusOrderToProperties(focusOrderReceiver))
 
-/**
- * Search up the component tree for any parent/parents that have specified a custom focus order.
- * Allowing parents higher up the hierarchy to overwrite the focus order specified by their
- * children.
- *
- * @param focusDirection the focus direction passed to [FocusManager.moveFocus] that triggered this
- * focus search.
- * @param layoutDirection the current system [LayoutDirection].
- */
-@OptIn(ExperimentalComposeUiApi::class)
-internal fun FocusTargetModifierNode.customFocusSearch(
-    focusDirection: FocusDirection,
-    layoutDirection: LayoutDirection
-): FocusRequester {
-    val focusProperties = fetchFocusProperties()
-    return when (focusDirection) {
-        Next -> focusProperties.next
-        Previous -> focusProperties.previous
-        Up -> focusProperties.up
-        Down -> focusProperties.down
-        Left -> when (layoutDirection) {
-            Ltr -> focusProperties.start
-            Rtl -> focusProperties.end
-        }.takeUnless { it == FocusRequester.Default } ?: focusProperties.left
-        Right -> when (layoutDirection) {
-            Ltr -> focusProperties.end
-            Rtl -> focusProperties.start
-        }.takeUnless { it == FocusRequester.Default } ?: focusProperties.right
-        // TODO(b/183746982): add focus order API for "In" and "Out".
-        //  Developers can to specify a custom "In" to specify which child should be visited when
-        //  the user presses dPad center. (They can also redirect the "In" to some other item).
-        //  Developers can specify a custom "Out" to specify which composable should take focus
-        //  when the user presses the back button.
-        @OptIn(ExperimentalComposeUiApi::class)
-        Enter -> {
-            @OptIn(ExperimentalComposeUiApi::class)
-            focusProperties.enter(focusDirection)
-        }
-        @OptIn(ExperimentalComposeUiApi::class)
-        Exit -> {
-            @OptIn(ExperimentalComposeUiApi::class)
-            focusProperties.exit(focusDirection)
-        }
-        else -> error("invalid FocusDirection")
-    }
-}
-
 @Suppress("DEPRECATION")
 internal class FocusOrderToProperties(
     val focusOrderReceiver: FocusOrder.() -> Unit
@@ -258,16 +200,3 @@
         focusOrderReceiver(FocusOrder(focusProperties))
     }
 }
-
-/**
- * Used internally for FocusOrderModifiers so that we can compare the modifiers and can reuse
- * the ModifierLocalConsumerEntity and ModifierLocalProviderEntity.
- */
-@Suppress("DEPRECATION")
-internal class FocusOrderModifierToProperties(
-    val modifier: FocusOrderModifier
-) : (FocusProperties) -> Unit {
-    override fun invoke(focusProperties: FocusProperties) {
-        modifier.populateFocusOrder(FocusOrder(focusProperties))
-    }
-}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
new file mode 100644
index 0000000..d3119d9
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection.Companion.Next
+import androidx.compose.ui.focus.FocusDirection.Companion.Previous
+import androidx.compose.ui.focus.FocusRequester.Companion.Cancel
+import androidx.compose.ui.focus.FocusRequester.Companion.Default
+import androidx.compose.ui.focus.FocusStateImpl.Active
+import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
+import androidx.compose.ui.focus.FocusStateImpl.Captured
+import androidx.compose.ui.focus.FocusStateImpl.Inactive
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.KeyInputModifierNode
+import androidx.compose.ui.input.rotary.RotaryScrollEvent
+import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.NodeKind
+import androidx.compose.ui.node.Nodes
+import androidx.compose.ui.node.ancestors
+import androidx.compose.ui.node.modifierElementOf
+import androidx.compose.ui.node.nearestAncestor
+import androidx.compose.ui.node.visitLocalChildren
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachReversed
+
+/**
+ * The focus manager is used by different [Owner][androidx.compose.ui.node.Owner] implementations
+ * to control focus.
+ */
+internal class FocusOwnerImpl(onRequestApplyChangesListener: (() -> Unit) -> Unit) : FocusOwner {
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    internal var rootFocusNode = FocusTargetModifierNode()
+
+    private val focusInvalidationManager = FocusInvalidationManager(onRequestApplyChangesListener)
+
+    /**
+     * A [Modifier] that can be added to the [Owners][androidx.compose.ui.node.Owner] modifier
+     * list that contains the modifiers required by the focus system. (Eg, a root focus modifier).
+     */
+    // TODO(b/168831247): return an empty Modifier when there are no focusable children.
+    @Suppress("ModifierInspectorInfo") // b/251831790.
+    @OptIn(ExperimentalComposeUiApi::class)
+    override val modifier: Modifier = modifierElementOf(
+        create = { rootFocusNode },
+        definitions = { name = "RootFocusTarget" }
+    )
+
+    override lateinit var layoutDirection: LayoutDirection
+
+    /**
+     * The [Owner][androidx.compose.ui.node.Owner] calls this function when it gains focus. This
+     * informs the [focus manager][FocusOwnerImpl] that the
+     * [Owner][androidx.compose.ui.node.Owner] gained focus, and that it should propagate this
+     * focus to one of the focus modifiers in the component hierarchy.
+     */
+    override fun takeFocus() {
+        // If the focus state is not Inactive, it indicates that the focus state is already
+        // set (possibly by dispatchWindowFocusChanged). So we don't update the state.
+        @OptIn(ExperimentalComposeUiApi::class)
+        if (rootFocusNode.focusStateImpl == Inactive) {
+            rootFocusNode.focusStateImpl = Active
+            // TODO(b/152535715): propagate focus to children based on child focusability.
+            //  moveFocus(FocusDirection.Enter)
+        }
+    }
+
+    /**
+     * The [Owner][androidx.compose.ui.node.Owner] calls this function when it loses focus. This
+     * informs the [focus manager][FocusOwnerImpl] that the
+     * [Owner][androidx.compose.ui.node.Owner] lost focus, and that it should clear focus from
+     * all the focus modifiers in the component hierarchy.
+     */
+    override fun releaseFocus() {
+        @OptIn(ExperimentalComposeUiApi::class)
+        rootFocusNode.clearFocus(forced = true, refreshFocusEvents = true)
+    }
+
+    /**
+     * Call this function to set the focus to the root focus modifier.
+     *
+     * @param force: Whether we should forcefully clear focus regardless of whether we have
+     * any components that have captured focus.
+     *
+     * This could be used to clear focus when a user clicks on empty space outside a focusable
+     * component.
+     */
+    override fun clearFocus(force: Boolean) {
+        clearFocus(force, refreshFocusEvents = true)
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    override fun clearFocus(force: Boolean, refreshFocusEvents: Boolean) {
+        // If this hierarchy had focus before clearing it, it indicates that the host view has
+        // focus. So after clearing focus within the compose hierarchy, we should restore focus to
+        // the root focus modifier to maintain consistency with the host view.
+        val rootInitialState = rootFocusNode.focusStateImpl
+        if (rootFocusNode.clearFocus(force, refreshFocusEvents)) {
+            rootFocusNode.focusStateImpl = when (rootInitialState) {
+                Active, ActiveParent, Captured -> Active
+                Inactive -> Inactive
+            }
+        }
+    }
+
+    /**
+     * Moves focus in the specified direction.
+     *
+     * @return true if focus was moved successfully. false if the focused item is unchanged.
+     */
+    @OptIn(ExperimentalComposeUiApi::class)
+    override fun moveFocus(focusDirection: FocusDirection): Boolean {
+
+        // If there is no active node in this sub-hierarchy, we can't move focus.
+        val source = rootFocusNode.findActiveFocusNode() ?: return false
+
+        // Check if a custom focus traversal order is specified.
+        when (val next = source.customFocusSearch(focusDirection, layoutDirection)) {
+            @OptIn(ExperimentalComposeUiApi::class)
+            Cancel -> return false
+            Default -> {
+                val foundNextItem =
+                    rootFocusNode.focusSearch(focusDirection, layoutDirection) { destination ->
+                        if (destination == source) return@focusSearch false
+                        checkNotNull(destination.nearestAncestor(Nodes.FocusTarget)) {
+                            "Focus search landed at the root."
+                        }
+                        // If we found a potential next item, move focus to it.
+                        destination.requestFocus()
+                    }
+                // If we didn't find a potential next item, try to wrap around.
+                return foundNextItem || wrapAroundFocus(focusDirection)
+            }
+            else -> return next.findFocusTarget { it.requestFocus() } ?: false
+        }
+    }
+
+    /**
+     * Dispatches a key event through the compose hierarchy.
+     */
+    @OptIn(ExperimentalComposeUiApi::class)
+    override fun dispatchKeyEvent(keyEvent: KeyEvent): Boolean {
+        val activeFocusTarget = rootFocusNode.findActiveFocusNode()
+        checkNotNull(activeFocusTarget) {
+            "Event can't be processed because we do not have an active focus target."
+        }
+        val focusedKeyInputNode = activeFocusTarget.lastLocalKeyInputNode()
+            ?: activeFocusTarget.nearestAncestor(Nodes.KeyInput)
+
+        focusedKeyInputNode?.traverseAncestors(
+            type = Nodes.KeyInput,
+            onPreVisit = { if (it.onPreKeyEvent(keyEvent)) return true },
+            onVisit = { if (it.onKeyEvent(keyEvent)) return true }
+        )
+
+        return false
+    }
+
+    /**
+     * Dispatches a rotary scroll event through the compose hierarchy.
+     */
+    @OptIn(ExperimentalComposeUiApi::class)
+    override fun dispatchRotaryEvent(event: RotaryScrollEvent): Boolean {
+        val focusedRotaryInputNode = rootFocusNode.findActiveFocusNode()
+            ?.nearestAncestor(Nodes.RotaryInput)
+
+        focusedRotaryInputNode?.traverseAncestors(
+            type = Nodes.RotaryInput,
+            onPreVisit = { if (it.onPreRotaryScrollEvent(event)) return true },
+            onVisit = { if (it.onRotaryScrollEvent(event)) return true }
+        )
+
+        return false
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    override fun scheduleInvalidation(node: FocusTargetModifierNode) {
+        focusInvalidationManager.scheduleInvalidation(node)
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    override fun scheduleInvalidation(node: FocusEventModifierNode) {
+        focusInvalidationManager.scheduleInvalidation(node)
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    override fun scheduleInvalidation(node: FocusPropertiesModifierNode) {
+        focusInvalidationManager.scheduleInvalidation(node)
+    }
+
+    @ExperimentalComposeUiApi
+    private inline fun <reified T : DelegatableNode> T.traverseAncestors(
+        type: NodeKind<T>,
+        onPreVisit: (T) -> Unit,
+        onVisit: (T) -> Unit
+    ) {
+        val ancestors = ancestors(type)
+        ancestors?.fastForEachReversed(onPreVisit)
+        onPreVisit(this)
+        onVisit(this)
+        ancestors?.fastForEach(onVisit)
+    }
+
+    /**
+     * Searches for the currently focused item, and returns its coordinates as a rect.
+     */
+    override fun getFocusRect(): Rect? {
+        @OptIn(ExperimentalComposeUiApi::class)
+        return rootFocusNode.findActiveFocusNode()?.focusRect()
+    }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    private fun DelegatableNode.lastLocalKeyInputNode(): KeyInputModifierNode? {
+        var focusedKeyInputNode: KeyInputModifierNode? = null
+        visitLocalChildren(Nodes.FocusTarget or Nodes.KeyInput) { modifierNode ->
+            if (modifierNode.isKind(Nodes.FocusTarget)) return focusedKeyInputNode
+
+            check(modifierNode is KeyInputModifierNode)
+            focusedKeyInputNode = modifierNode
+        }
+        return focusedKeyInputNode
+    }
+
+    // TODO(b/144116848): This is a hack to make Next/Previous wrap around. This must be
+    //  replaced by code that sends the move request back to the view system. The view system
+    //  will then pass focus to other views, and ultimately return back to this compose view.
+    private fun wrapAroundFocus(focusDirection: FocusDirection): Boolean {
+        // Wrap is not supported when this sub-hierarchy doesn't have focus.
+        @OptIn(ExperimentalComposeUiApi::class)
+        if (!rootFocusNode.focusState.hasFocus || rootFocusNode.focusState.isFocused) return false
+
+        // Next and Previous wraps around.
+        when (focusDirection) {
+            Next, Previous -> {
+                // Clear Focus to send focus the root node.
+                clearFocus(force = false)
+                @OptIn(ExperimentalComposeUiApi::class)
+                if (!rootFocusNode.focusState.isFocused) return false
+
+                // Wrap around by calling moveFocus after the root gains focus.
+                return moveFocus(focusDirection)
+            }
+            // We only wrap-around for 1D Focus search.
+            else -> return false
+        }
+    }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusProperties.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusProperties.kt
index 5dd4a6e..446f85c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusProperties.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusProperties.kt
@@ -18,26 +18,7 @@
 
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.node.DelegatableNode
-import androidx.compose.ui.node.Nodes
 import androidx.compose.ui.node.modifierElementOf
-import androidx.compose.ui.node.requireOwner
-import androidx.compose.ui.node.visitChildren
-
-/**
- * Implement this interface create a modifier node that can be used to modify the focus properties
- * of the associated [FocusTargetModifierNode].
- */
-@ExperimentalComposeUiApi
-interface FocusPropertiesModifierNode : DelegatableNode {
-    /**
-     * A parent can modify the focus properties associated with the nearest
-     * [FocusTargetModifierNode] child node. If a [FocusTargetModifierNode] has multiple parent
-     * [FocusPropertiesModifierNode]s, properties set by a parent higher up in the hierarchy
-     * overwrite properties set by those that are lower in the hierarchy.
-     */
-    fun modifyFocusProperties(focusProperties: FocusProperties)
-}
 
 /**
  * Properties that are applied to [focusTarget] that is the first child of the
@@ -173,36 +154,6 @@
         set(_) {}
 }
 
-/**
- * This modifier allows you to specify properties that are accessible to [focusTarget]s further
- * down the modifier chain or on child layout nodes.
- *
- * @sample androidx.compose.ui.samples.FocusPropertiesSample
- */
-@Suppress("ModifierInspectorInfo") // b/251831790.
-fun Modifier.focusProperties(scope: FocusProperties.() -> Unit): Modifier = this.then(
-    @OptIn(ExperimentalComposeUiApi::class)
-    modifierElementOf(
-        key = scope,
-        create = { FocusPropertiesModifierNodeImpl(scope) },
-        update = { it.focusPropertiesScope = scope },
-        definitions = {
-            name = "focusProperties"
-            properties["scope"] = scope
-        }
-    )
-)
-
-@OptIn(ExperimentalComposeUiApi::class)
-internal class FocusPropertiesModifierNodeImpl(
-    internal var focusPropertiesScope: FocusProperties.() -> Unit,
-) : FocusPropertiesModifierNode, Modifier.Node() {
-
-    override fun modifyFocusProperties(focusProperties: FocusProperties) {
-        focusProperties.apply(focusPropertiesScope)
-    }
-}
-
 internal class FocusPropertiesImpl : FocusProperties {
     override var canFocus: Boolean = true
     override var next: FocusRequester = FocusRequester.Default
@@ -219,11 +170,32 @@
     override var exit: (FocusDirection) -> FocusRequester = { FocusRequester.Default }
 }
 
-@ExperimentalComposeUiApi
-internal fun FocusPropertiesModifierNode.scheduleInvalidationOfAssociatedFocusTargets() {
-    visitChildren(Nodes.FocusTarget) {
-        // Schedule invalidation for the focus target,
-        // which will cause it to recalculate focus properties.
-        requireOwner().focusOwner.scheduleInvalidation(it)
+/**
+ * This modifier allows you to specify properties that are accessible to [focusTarget]s further
+ * down the modifier chain or on child layout nodes.
+ *
+ * @sample androidx.compose.ui.samples.FocusPropertiesSample
+ */
+@Suppress("ModifierInspectorInfo") // b/251831790.
+fun Modifier.focusProperties(scope: FocusProperties.() -> Unit): Modifier = this.then(
+    @OptIn(ExperimentalComposeUiApi::class)
+    (modifierElementOf(
+        key = scope,
+        create = { FocusPropertiesModifierNodeImpl(scope) },
+        update = { it.focusPropertiesScope = scope },
+        definitions = {
+            name = "focusProperties"
+            properties["scope"] = scope
+        }
+    ))
+)
+
+@OptIn(ExperimentalComposeUiApi::class)
+private class FocusPropertiesModifierNodeImpl(
+    var focusPropertiesScope: FocusProperties.() -> Unit,
+) : FocusPropertiesModifierNode, Modifier.Node() {
+
+    override fun modifyFocusProperties(focusProperties: FocusProperties) {
+        focusProperties.apply(focusPropertiesScope)
     }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusPropertiesModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusPropertiesModifierNode.kt
new file mode 100644
index 0000000..4bfb10f
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusPropertiesModifierNode.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.node.DelegatableNode
+
+/**
+ * Implement this interface create a modifier node that can be used to modify the focus properties
+ * of the associated [FocusTargetModifierNode].
+ */
+@ExperimentalComposeUiApi
+interface FocusPropertiesModifierNode : DelegatableNode {
+    /**
+     * A parent can modify the focus properties associated with the nearest
+     * [FocusTargetModifierNode] child node. If a [FocusTargetModifierNode] has multiple parent
+     * [FocusPropertiesModifierNode]s, properties set by a parent higher up in the hierarchy
+     * overwrite properties set by those that are lower in the hierarchy.
+     */
+    fun modifyFocusProperties(focusProperties: FocusProperties)
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
index 2c66b55..d5bbb48 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.focus
 
+import androidx.compose.runtime.Stable
 import androidx.compose.runtime.collection.MutableVector
 import androidx.compose.runtime.collection.mutableVectorOf
 import androidx.compose.ui.ExperimentalComposeUiApi
@@ -40,6 +41,7 @@
  *
  * @see androidx.compose.ui.focus.focusRequester
  */
+@Stable
 class FocusRequester {
 
     @OptIn(ExperimentalComposeUiApi::class)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifier.kt
index 757a52a..4af7a97 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifier.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.
@@ -18,18 +18,7 @@
 
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.internal.JvmDefaultWithCompatibility
-import androidx.compose.ui.node.DelegatableNode
-import androidx.compose.ui.node.Nodes
 import androidx.compose.ui.node.modifierElementOf
-import androidx.compose.ui.node.visitChildren
-
-/**
- * Implement this interface to create a modifier node that can be used to request changes in
- * the focus state of a [FocusTargetModifierNode] down the hierarchy.
- */
-@ExperimentalComposeUiApi
-interface FocusRequesterModifierNode : DelegatableNode
 
 /**
  * A [modifier][Modifier.Element] that is used to pass in a [FocusRequester] that can be used to
@@ -52,70 +41,30 @@
 }
 
 /**
- * Use this function to request focus. If the system grants focus to a component associated
- * with this [FocusRequester], its [onFocusChanged] modifiers will receive a [FocusState] object
- * where [FocusState.isFocused] is true.
+ * Add this modifier to a component to request changes to focus.
  *
  * @sample androidx.compose.ui.samples.RequestFocusSample
  */
-@ExperimentalComposeUiApi
-fun FocusRequesterModifierNode.requestFocus(): Boolean {
-    visitChildren(Nodes.FocusTarget) {
-        if (it.requestFocus()) return true
-    }
-    return false
-}
-
-/**
- * Deny requests to clear focus.
- *
- * Use this function to send a request to capture focus. If a component captures focus,
- * it will send a [FocusState] object to its associated [onFocusChanged]
- * modifiers where [FocusState.isCaptured]() == true.
- *
- * When a component is in a Captured state, all focus requests from other components are
- * declined.
- *
- * @return true if the focus was successfully captured by one of the
- * [focus][focusTarget] modifiers associated with this [FocusRequester]. False otherwise.
- *
- * @sample androidx.compose.ui.samples.CaptureFocusSample
- */
-@ExperimentalComposeUiApi
-fun FocusRequesterModifierNode.captureFocus(): Boolean {
-    visitChildren(Nodes.FocusTarget) {
-        if (it.captureFocus()) {
-            // it.refreshFocusEventNodes()
-            return true
+@Suppress("ModifierInspectorInfo") // b/251831790.
+fun Modifier.focusRequester(focusRequester: FocusRequester): Modifier = this.then(
+    @OptIn(ExperimentalComposeUiApi::class)
+    (modifierElementOf(
+        key = focusRequester,
+        create = { FocusRequesterModifierNodeImpl(focusRequester) },
+        update = {
+            it.focusRequester.focusRequesterNodes -= it
+            it.focusRequester = focusRequester
+            it.focusRequester.focusRequesterNodes += it
+        },
+        definitions = {
+            name = "focusRequester"
+            properties["focusRequester"] = focusRequester
         }
-    }
-    return false
-}
-
-/**
- * Use this function to send a request to free focus when one of the components associated
- * with this [FocusRequester] is in a Captured state. If a component frees focus,
- * it will send a [FocusState] object to its associated [onFocusChanged]
- * modifiers where [FocusState.isCaptured]() == false.
- *
- * When a component is in a Captured state, all focus requests from other components are
- * declined.
- *.
- * @return true if the captured focus was successfully released. i.e. At the end of this
- * operation, one of the components associated with this [focusRequester] freed focus.
- *
- * @sample androidx.compose.ui.samples.CaptureFocusSample
- */
-@ExperimentalComposeUiApi
-fun FocusRequesterModifierNode.freeFocus(): Boolean {
-    visitChildren(Nodes.FocusTarget) {
-        if (it.freeFocus()) return true
-    }
-    return false
-}
+    ))
+)
 
 @OptIn(ExperimentalComposeUiApi::class)
-internal class FocusRequesterModifierNodeImpl(
+private class FocusRequesterModifierNodeImpl(
     var focusRequester: FocusRequester
 ) : FocusRequesterModifierNode, Modifier.Node() {
     override fun onAttach() {
@@ -128,26 +77,3 @@
         super.onDetach()
     }
 }
-
-/**
- * Add this modifier to a component to request changes to focus.
- *
- * @sample androidx.compose.ui.samples.RequestFocusSample
- */
-@Suppress("ModifierInspectorInfo") // b/251831790.
-fun Modifier.focusRequester(focusRequester: FocusRequester): Modifier = this.then(
-    @OptIn(ExperimentalComposeUiApi::class)
-    modifierElementOf(
-        key = focusRequester,
-        create = { FocusRequesterModifierNodeImpl(focusRequester) },
-        update = {
-            it.focusRequester.focusRequesterNodes -= it
-            it.focusRequester = focusRequester
-            it.focusRequester.focusRequesterNodes += it
-        },
-        definitions = {
-            name = "focusRequester"
-            properties["focusRequester"] = focusRequester
-        }
-    )
-)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifierNode.kt
new file mode 100644
index 0000000..8b3c1e6
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequesterModifierNode.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.Nodes
+import androidx.compose.ui.node.visitChildren
+
+/**
+ * Implement this interface to create a modifier node that can be used to request changes in
+ * the focus state of a [FocusTargetModifierNode] down the hierarchy.
+ */
+@ExperimentalComposeUiApi
+interface FocusRequesterModifierNode : DelegatableNode
+
+/**
+ * Use this function to request focus. If the system grants focus to a component associated
+ * with this [FocusRequester], its [onFocusChanged] modifiers will receive a [FocusState] object
+ * where [FocusState.isFocused] is true.
+ *
+ * @sample androidx.compose.ui.samples.RequestFocusSample
+ */
+@ExperimentalComposeUiApi
+fun FocusRequesterModifierNode.requestFocus(): Boolean {
+    visitChildren(Nodes.FocusTarget) {
+        if (it.requestFocus()) return true
+    }
+    return false
+}
+
+/**
+ * Deny requests to clear focus.
+ *
+ * Use this function to send a request to capture focus. If a component captures focus,
+ * it will send a [FocusState] object to its associated [onFocusChanged]
+ * modifiers where [FocusState.isCaptured]() == true.
+ *
+ * When a component is in a Captured state, all focus requests from other components are
+ * declined.
+ *
+ * @return true if the focus was successfully captured by one of the
+ * [focus][focusTarget] modifiers associated with this [FocusRequester]. False otherwise.
+ *
+ * @sample androidx.compose.ui.samples.CaptureFocusSample
+ */
+@ExperimentalComposeUiApi
+fun FocusRequesterModifierNode.captureFocus(): Boolean {
+    visitChildren(Nodes.FocusTarget) {
+        if (it.captureFocus()) {
+            return true
+        }
+    }
+    return false
+}
+
+/**
+ * Use this function to send a request to free focus when one of the components associated
+ * with this [FocusRequester] is in a Captured state. If a component frees focus,
+ * it will send a [FocusState] object to its associated [onFocusChanged]
+ * modifiers where [FocusState.isCaptured]() == false.
+ *
+ * When a component is in a Captured state, all focus requests from other components are
+ * declined.
+ *.
+ * @return true if the captured focus was successfully released. i.e. At the end of this
+ * operation, one of the components associated with this [focusRequester] freed focus.
+ *
+ * @sample androidx.compose.ui.samples.CaptureFocusSample
+ */
+@ExperimentalComposeUiApi
+fun FocusRequesterModifierNode.freeFocus(): Boolean {
+    visitChildren(Nodes.FocusTarget) {
+        if (it.freeFocus()) return true
+    }
+    return false
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.kt
new file mode 100644
index 0000000..61519df
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetModifierNode.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.ui.focus
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusStateImpl.Active
+import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
+import androidx.compose.ui.focus.FocusStateImpl.Captured
+import androidx.compose.ui.focus.FocusStateImpl.Inactive
+import androidx.compose.ui.layout.BeyondBoundsLayout
+import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
+import androidx.compose.ui.modifier.ModifierLocalNode
+import androidx.compose.ui.node.Nodes
+import androidx.compose.ui.node.ObserverNode
+import androidx.compose.ui.node.modifierElementOf
+import androidx.compose.ui.node.observeReads
+import androidx.compose.ui.node.requireOwner
+import androidx.compose.ui.node.visitAncestors
+
+/**
+ * This modifier node can be used to create a modifier that makes a component focusable.
+ * Use a different instance of [FocusTargetModifierNode] for each focusable component.
+ */
+@ExperimentalComposeUiApi
+class FocusTargetModifierNode : ObserverNode, ModifierLocalNode, Modifier.Node() {
+    /**
+     * The [FocusState] associated with this [FocusTargetModifierNode].
+     */
+    val focusState: FocusState
+        get() = focusStateImpl
+
+    internal var focusStateImpl = Inactive
+    internal val beyondBoundsLayoutParent: BeyondBoundsLayout?
+        get() = ModifierLocalBeyondBoundsLayout.current
+
+    override fun onObservedReadsChanged() {
+        val previousFocusState = focusState
+        invalidateFocus()
+        if (previousFocusState != focusState) refreshFocusEventNodes()
+    }
+
+    internal fun onRemoved() {
+        when (focusState) {
+            // Clear focus from the current FocusTarget.
+            // This currently clears focus from the entire hierarchy, but we can change the
+            // implementation so that focus is sent to the immediate focus parent.
+            Active, Captured -> requireOwner().focusOwner.clearFocus(force = true)
+
+            ActiveParent, Inactive -> scheduleInvalidationForFocusEvents()
+        }
+    }
+
+    /**
+     * Visits parent [FocusPropertiesModifierNode]s and runs
+     * [FocusPropertiesModifierNode.modifyFocusProperties] on each parent.
+     * This effectively collects an aggregated focus state.
+     */
+    @ExperimentalComposeUiApi
+    internal fun fetchFocusProperties(): FocusProperties {
+        val properties = FocusPropertiesImpl()
+        visitAncestors(Nodes.FocusProperties or Nodes.FocusTarget) {
+            // If we reach the previous default focus properties node, we have gone too far, as
+            //  this is applies to the parent focus modifier.
+            if (it.isKind(Nodes.FocusTarget)) return properties
+
+            // Parent can override any values set by this
+            check(it is FocusPropertiesModifierNode)
+            it.modifyFocusProperties(properties)
+        }
+        return properties
+    }
+
+    internal fun invalidateFocus() {
+        when (focusState) {
+            // Clear focus from the current FocusTarget.
+            // This currently clears focus from the entire hierarchy, but we can change the
+            // implementation so that focus is sent to the immediate focus parent.
+            Active, Captured -> {
+                lateinit var focusProperties: FocusProperties
+                observeReads {
+                    focusProperties = fetchFocusProperties()
+                }
+                if (!focusProperties.canFocus) {
+                    requireOwner().focusOwner.clearFocus(force = true)
+                }
+            }
+
+            ActiveParent, Inactive -> {}
+        }
+    }
+
+    internal fun scheduleInvalidationForFocusEvents() {
+        visitAncestors(Nodes.FocusEvent or Nodes.FocusTarget) {
+            if (it.isKind(Nodes.FocusTarget)) return@visitAncestors
+
+            check(it is FocusEventModifierNode)
+            requireOwner().focusOwner.scheduleInvalidation(it)
+        }
+    }
+
+    internal companion object {
+        internal val FocusTargetModifierElement = modifierElementOf(
+            create = { FocusTargetModifierNode() },
+            definitions = { name = "focusTarget" }
+        )
+    }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
index c5e7f0b..68a42ed 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.focus
 
-import androidx.compose.runtime.collection.MutableVector
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.focus.FocusDirection.Companion.Down
 import androidx.compose.ui.focus.FocusDirection.Companion.Enter
@@ -26,7 +25,6 @@
 import androidx.compose.ui.focus.FocusDirection.Companion.Previous
 import androidx.compose.ui.focus.FocusDirection.Companion.Right
 import androidx.compose.ui.focus.FocusDirection.Companion.Up
-import androidx.compose.ui.focus.FocusRequester.Companion.Cancel
 import androidx.compose.ui.focus.FocusRequester.Companion.Default
 import androidx.compose.ui.focus.FocusStateImpl.Active
 import androidx.compose.ui.focus.FocusStateImpl.ActiveParent
@@ -34,136 +32,57 @@
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.layout.findRootCoordinates
-import androidx.compose.ui.node.DelegatableNode
 import androidx.compose.ui.node.Nodes
 import androidx.compose.ui.node.visitAncestors
 import androidx.compose.ui.node.visitChildren
-import androidx.compose.ui.node.visitSubtreeIf
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.LayoutDirection.Ltr
 import androidx.compose.ui.unit.LayoutDirection.Rtl
 
-private const val invalidFocusDirection = "Invalid FocusDirection"
-
 /**
- * The [FocusDirection] is used to specify the direction for a [FocusManager.moveFocus]
- * request.
+ * Search up the component tree for any parent/parents that have specified a custom focus order.
+ * Allowing parents higher up the hierarchy to overwrite the focus order specified by their
+ * children.
  *
- * @sample androidx.compose.ui.samples.MoveFocusSample
+ * @param focusDirection the focus direction passed to [FocusManager.moveFocus] that triggered this
+ * focus search.
+ * @param layoutDirection the current system [LayoutDirection].
  */
-@kotlin.jvm.JvmInline
-value class FocusDirection internal constructor(@Suppress("unused") private val value: Int) {
-
-    override fun toString(): String {
-        return when (this) {
-            Next -> "Next"
-            Previous -> "Previous"
-            Left -> "Left"
-            Right -> "Right"
-            Up -> "Up"
-            Down -> "Down"
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun FocusTargetModifierNode.customFocusSearch(
+    focusDirection: FocusDirection,
+    layoutDirection: LayoutDirection
+): FocusRequester {
+    val focusProperties = fetchFocusProperties()
+    return when (focusDirection) {
+        Next -> focusProperties.next
+        Previous -> focusProperties.previous
+        Up -> focusProperties.up
+        Down -> focusProperties.down
+        Left -> when (layoutDirection) {
+            Ltr -> focusProperties.start
+            Rtl -> focusProperties.end
+        }.takeUnless { it == Default } ?: focusProperties.left
+        Right -> when (layoutDirection) {
+            Ltr -> focusProperties.end
+            Rtl -> focusProperties.start
+        }.takeUnless { it == Default } ?: focusProperties.right
+        // TODO(b/183746982): add focus order API for "In" and "Out".
+        //  Developers can to specify a custom "In" to specify which child should be visited when
+        //  the user presses dPad center. (They can also redirect the "In" to some other item).
+        //  Developers can specify a custom "Out" to specify which composable should take focus
+        //  when the user presses the back button.
+        @OptIn(ExperimentalComposeUiApi::class)
+        Enter -> {
             @OptIn(ExperimentalComposeUiApi::class)
-            Enter -> "Enter"
-            @OptIn(ExperimentalComposeUiApi::class)
-            Exit -> "Exit"
-            else -> invalidFocusDirection
+            focusProperties.enter(focusDirection)
         }
-    }
-
-    companion object {
-        /**
-         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
-         *  next focusable item.
-         *
-         *  @sample androidx.compose.ui.samples.MoveFocusSample
-         */
-        val Next: FocusDirection = FocusDirection(1)
-
-        /**
-         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
-         *  previous focusable item.
-         *
-         *  @sample androidx.compose.ui.samples.MoveFocusSample
-         */
-        val Previous: FocusDirection = FocusDirection(2)
-
-        /**
-         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
-         *  next focusable item to the left of the currently focused item.
-         *
-         *  @sample androidx.compose.ui.samples.MoveFocusSample
-         */
-        val Left: FocusDirection = FocusDirection(3)
-
-        /**
-         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
-         *  next focusable item to the right of the currently focused item.
-         *
-         *  @sample androidx.compose.ui.samples.MoveFocusSample
-         */
-        val Right: FocusDirection = FocusDirection(4)
-
-        /**
-         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
-         *  next focusable item that is above the currently focused item.
-         *
-         *  @sample androidx.compose.ui.samples.MoveFocusSample
-         */
-        val Up: FocusDirection = FocusDirection(5)
-
-        /**
-         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
-         *  next focusable item that is below the currently focused item.
-         *
-         *  @sample androidx.compose.ui.samples.MoveFocusSample
-         */
-        val Down: FocusDirection = FocusDirection(6)
-
-        /**
-         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
-         *  next focusable item that is a child of the currently focused item.
-         */
-        @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
-        @get:ExperimentalComposeUiApi
-        @ExperimentalComposeUiApi
-        val Enter: FocusDirection = FocusDirection(7)
-
-        /**
-         *  Direction used in [FocusManager.moveFocus] to indicate that you want to move focus to
-         *  the parent of the currently focused item.
-         */
-        @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
-        @get:ExperimentalComposeUiApi
-        @ExperimentalComposeUiApi
-        val Exit: FocusDirection = FocusDirection(8)
-
-        /**
-         *  Direction used in [FocusManager.moveFocus] to indicate that you are searching for the
-         *  next focusable item that is a child of the currently focused item.
-         */
-        @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET", "Unused")
-        @get:ExperimentalComposeUiApi
-        @ExperimentalComposeUiApi
-        @Deprecated(
-            "Use FocusDirection.Enter instead.",
-            ReplaceWith("Enter", "androidx.compose.ui.focus.FocusDirection.Companion.Enter"),
-            DeprecationLevel.WARNING
-        )
-        val In: FocusDirection = Enter
-
-        /**
-         *  Direction used in [FocusManager.moveFocus] to indicate that you want to move focus to
-         *  the parent of the currently focused item.
-         */
-        @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET", "Unused")
-        @get:ExperimentalComposeUiApi
-        @ExperimentalComposeUiApi
-        @Deprecated(
-            "Use FocusDirection.Exit instead.",
-            ReplaceWith("Exit", "androidx.compose.ui.focus.FocusDirection.Companion.Exit"),
-            DeprecationLevel.WARNING
-        )
-        val Out: FocusDirection = Exit
+        @OptIn(ExperimentalComposeUiApi::class)
+        Exit -> {
+            @OptIn(ExperimentalComposeUiApi::class)
+            focusProperties.exit(focusDirection)
+        }
+        else -> error("invalid FocusDirection")
     }
 }
 
@@ -194,33 +113,10 @@
         Exit -> findActiveFocusNode()?.findNonDeactivatedParent().let {
             if (it == null || it == this) false else onFound.invoke(it)
         }
-        else -> error(invalidFocusDirection)
+        else -> error("Focus search invoked with invalid FocusDirection $focusDirection")
     }
 }
 
-@OptIn(ExperimentalComposeUiApi::class)
-internal fun FocusTargetModifierNode.findActiveFocusNode(): FocusTargetModifierNode? {
-    when (focusStateImpl) {
-        Active, Captured -> return this
-        ActiveParent -> {
-            visitChildren(Nodes.FocusTarget) { node ->
-                node.findActiveFocusNode()?.let { return it }
-            }
-            return null
-        }
-        Inactive -> return null
-    }
-}
-
-@Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
-@OptIn(ExperimentalComposeUiApi::class)
-internal fun FocusTargetModifierNode.findNonDeactivatedParent(): FocusTargetModifierNode? {
-    visitAncestors(Nodes.FocusTarget) {
-        if (it.fetchFocusProperties().canFocus) return it
-    }
-    return null
-}
-
 /**
  * Returns the bounding box of the focus layout area in the root or [Rect.Zero] if the
  * FocusModifier has not had a layout.
@@ -231,38 +127,6 @@
 } ?: Rect.Zero
 
 /**
- * Returns all [FocusTargetModifierNode] children that are not Deactivated. Any
- * child that is deactivated will add activated children instead, unless the deactivated
- * node has a custom Enter specified.
- */
-@ExperimentalComposeUiApi
-internal fun DelegatableNode.collectAccessibleChildren(
-    accessibleChildren: MutableVector<FocusTargetModifierNode>
-) {
-    visitSubtreeIf(Nodes.FocusTarget) {
-
-        if (it.fetchFocusProperties().canFocus) {
-            accessibleChildren.add(it)
-            return@visitSubtreeIf false
-        }
-
-        // If we encounter a deactivated child, we mimic a moveFocus(Enter).
-        when (val customEnter = it.fetchFocusProperties().enter(Enter)) {
-            // If the user declined a custom enter, omit this part of the tree.
-            Cancel -> return@visitSubtreeIf false
-
-            // If there is no custom enter, we consider all the children.
-            Default -> return@visitSubtreeIf true
-
-            else -> customEnter.focusRequesterNodes.forEach { node ->
-                node.collectAccessibleChildren(accessibleChildren)
-            }
-        }
-        false
-    }
-}
-
-/**
  * Whether this node should be considered when searching for the next item during a traversal.
  */
 @ExperimentalComposeUiApi
@@ -283,3 +147,26 @@
         }
         return null
     }
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun FocusTargetModifierNode.findActiveFocusNode(): FocusTargetModifierNode? {
+    when (focusStateImpl) {
+        Active, Captured -> return this
+        ActiveParent -> {
+            visitChildren(Nodes.FocusTarget) { node ->
+                node.findActiveFocusNode()?.let { return it }
+            }
+            return null
+        }
+        Inactive -> return null
+    }
+}
+
+@Suppress("ModifierFactoryExtensionFunction", "ModifierFactoryReturnType")
+@OptIn(ExperimentalComposeUiApi::class)
+private fun FocusTargetModifierNode.findNonDeactivatedParent(): FocusTargetModifierNode? {
+    visitAncestors(Nodes.FocusTarget) {
+        if (it.fetchFocusProperties().canFocus) return it
+    }
+    return null
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
index 55655cb..2007a4c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
@@ -28,8 +28,10 @@
 import androidx.compose.ui.focus.FocusStateImpl.Captured
 import androidx.compose.ui.focus.FocusStateImpl.Inactive
 import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.node.DelegatableNode
 import androidx.compose.ui.node.Nodes
 import androidx.compose.ui.node.visitChildren
+import androidx.compose.ui.node.visitSubtreeIf
 import kotlin.math.absoluteValue
 import kotlin.math.max
 
@@ -189,6 +191,38 @@
     return false
 }
 
+/**
+ * Returns all [FocusTargetModifierNode] children that are not Deactivated. Any
+ * child that is deactivated will add activated children instead, unless the deactivated
+ * node has a custom Enter specified.
+ */
+@ExperimentalComposeUiApi
+private fun DelegatableNode.collectAccessibleChildren(
+    accessibleChildren: MutableVector<FocusTargetModifierNode>
+) {
+    visitSubtreeIf(Nodes.FocusTarget) {
+
+        if (it.fetchFocusProperties().canFocus) {
+            accessibleChildren.add(it)
+            return@visitSubtreeIf false
+        }
+
+        // If we encounter a deactivated child, we mimic a moveFocus(Enter).
+        when (val customEnter = it.fetchFocusProperties().enter(Enter)) {
+            // If the user declined a custom enter, omit this part of the tree.
+            FocusRequester.Cancel -> return@visitSubtreeIf false
+
+            // If there is no custom enter, we consider all the children.
+            FocusRequester.Default -> return@visitSubtreeIf true
+
+            else -> customEnter.focusRequesterNodes.forEach { node ->
+                node.collectAccessibleChildren(accessibleChildren)
+            }
+        }
+        false
+    }
+}
+
 // Iterate through this list of focus nodes and find best candidate in the specified direction.
 // TODO(b/182319711): For Left/Right focus moves, Consider finding the first candidate in the beam
 //  and then only comparing candidates in the beam. If nothing is in the beam, then consider all
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/ScrollContainerInfo.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/ScrollContainerInfo.kt
deleted file mode 100644
index 2e8afd7..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/ScrollContainerInfo.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.ui.input
-
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalProvider
-import androidx.compose.ui.modifier.ModifierLocalReadScope
-import androidx.compose.ui.modifier.ProvidableModifierLocal
-import androidx.compose.ui.modifier.modifierLocalConsumer
-import androidx.compose.ui.modifier.modifierLocalOf
-import androidx.compose.ui.platform.debugInspectorInfo
-
-/**
- * Represents a component that handles scroll events, so that other components in the hierarchy
- * can adjust their behaviour.
- * @See [provideScrollContainerInfo] and [consumeScrollContainerInfo]
- */
-interface ScrollContainerInfo {
-    /** @return whether this component handles horizontal scroll events */
-    fun canScrollHorizontally(): Boolean
-    /** @return whether this component handles vertical scroll events */
-    fun canScrollVertically(): Boolean
-}
-
-/** @return whether this container handles either horizontal or vertical scroll events */
-fun ScrollContainerInfo.canScroll() = canScrollVertically() || canScrollHorizontally()
-
-/**
- * A modifier to query whether there are any parents in the hierarchy that handle scroll events.
- * The [ScrollContainerInfo] provided in [consumer] will recursively look for ancestors if the
- * nearest parent does not handle scroll events in the queried direction.
- * This can be used to delay UI changes in cases where a pointer event may later become a scroll,
- * cancelling any existing press or other gesture.
- *
- * @sample androidx.compose.ui.samples.ScrollableContainerSample
- */
-@OptIn(ExperimentalComposeUiApi::class)
-fun Modifier.consumeScrollContainerInfo(consumer: (ScrollContainerInfo?) -> Unit): Modifier =
-    modifierLocalConsumer {
-        consumer(ModifierLocalScrollContainerInfo.current)
-    }
-
-/**
- * A Modifier that indicates that this component handles scroll events. Use
- * [consumeScrollContainerInfo] to query whether there is a parent in the hierarchy that is
- * a [ScrollContainerInfo].
- */
-fun Modifier.provideScrollContainerInfo(scrollContainerInfo: ScrollContainerInfo): Modifier =
-    composed(
-        inspectorInfo = debugInspectorInfo {
-            name = "provideScrollContainerInfo"
-            value = scrollContainerInfo
-        }) {
-    remember(scrollContainerInfo) {
-        ScrollContainerInfoModifierLocal(scrollContainerInfo)
-    }
-}
-
-/**
- * ModifierLocal to propagate ScrollableContainer throughout the hierarchy.
- * This Modifier will recursively check for ancestor ScrollableContainers,
- * if the current ScrollableContainer does not handle scroll events in a particular direction.
- */
-private class ScrollContainerInfoModifierLocal(
-    private val scrollContainerInfo: ScrollContainerInfo,
-) : ScrollContainerInfo, ModifierLocalProvider<ScrollContainerInfo?>, ModifierLocalConsumer {
-
-    private var parent: ScrollContainerInfo? by mutableStateOf(null)
-
-    override val key: ProvidableModifierLocal<ScrollContainerInfo?> =
-        ModifierLocalScrollContainerInfo
-    override val value: ScrollContainerInfoModifierLocal = this
-
-    override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) = with(scope) {
-        parent = ModifierLocalScrollContainerInfo.current
-    }
-
-    override fun canScrollHorizontally(): Boolean {
-        return scrollContainerInfo.canScrollHorizontally() ||
-            parent?.canScrollHorizontally() == true
-    }
-
-    override fun canScrollVertically(): Boolean {
-        return scrollContainerInfo.canScrollVertically() || parent?.canScrollVertically() == true
-    }
-}
-
-internal val ModifierLocalScrollContainerInfo = modifierLocalOf<ScrollContainerInfo?> {
-    null
-}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
index ef60df1..4fa07ad 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
@@ -18,47 +18,9 @@
 
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.node.DelegatableNode
 import androidx.compose.ui.node.modifierElementOf
 
 /**
- * Implement this interface to create a [Modifier.Node] that can intercept hardware Key events.
- *
- * The event is routed to the focused item. Before reaching the focused item, [onPreKeyEvent]() is
- * called for parents of the focused item. If the parents don't consume the event, [onPreKeyEvent]()
- * is called for the focused item. If the event is still not consumed, [onKeyEvent]() is called on
- * the focused item's parents.
- */
-@ExperimentalComposeUiApi
-interface KeyInputModifierNode : DelegatableNode {
-
-    /**
-     * This function is called when a [KeyEvent] is received by this node during the upward
-     * pass. While implementing this callback, return true to stop propagation of this event. If you
-     * return false, the key event will be sent to this [KeyInputModifierNode]'s parent.
-     */
-    fun onKeyEvent(event: KeyEvent): Boolean
-
-    /**
-     * This function is called when a [KeyEvent] is received by this node during the
-     * downward pass. It gives ancestors of a focused component the chance to intercept an event.
-     * Return true to stop propagation of this event. If you return false, the event will be sent
-     * to this [KeyInputModifierNode]'s child. If none of the children consume the event,
-     * it will be sent back up to the root using the [onKeyEvent] function.
-     */
-    fun onPreKeyEvent(event: KeyEvent): Boolean
-}
-
-@ExperimentalComposeUiApi
-internal class KeyInputInputModifierNodeImpl(
-    var onEvent: ((KeyEvent) -> Boolean)?,
-    var onPreEvent: ((KeyEvent) -> Boolean)?
-) : KeyInputModifierNode, Modifier.Node() {
-    override fun onKeyEvent(event: KeyEvent): Boolean = this.onEvent?.invoke(event) ?: false
-    override fun onPreKeyEvent(event: KeyEvent): Boolean = this.onPreEvent?.invoke(event) ?: false
-}
-
-/**
  * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
  * allow it to intercept hardware key events when it (or one of its children) is focused.
  *
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifierNode.kt
new file mode 100644
index 0000000..47ad734
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifierNode.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.input.key
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.DelegatableNode
+
+/**
+ * Implement this interface to create a [Modifier.Node] that can intercept hardware Key events.
+ *
+ * The event is routed to the focused item. Before reaching the focused item, [onPreKeyEvent]() is
+ * called for parents of the focused item. If the parents don't consume the event, [onPreKeyEvent]()
+ * is called for the focused item. If the event is still not consumed, [onKeyEvent]() is called on
+ * the focused item's parents.
+ */
+@ExperimentalComposeUiApi
+interface KeyInputModifierNode : DelegatableNode {
+
+    /**
+     * This function is called when a [KeyEvent] is received by this node during the upward
+     * pass. While implementing this callback, return true to stop propagation of this event. If you
+     * return false, the key event will be sent to this [KeyInputModifierNode]'s parent.
+     */
+    fun onKeyEvent(event: KeyEvent): Boolean
+
+    /**
+     * This function is called when a [KeyEvent] is received by this node during the
+     * downward pass. It gives ancestors of a focused component the chance to intercept an event.
+     * Return true to stop propagation of this event. If you return false, the event will be sent
+     * to this [KeyInputModifierNode]'s child. If none of the children consume the event,
+     * it will be sent back up to the root using the [onKeyEvent] function.
+     */
+    fun onPreKeyEvent(event: KeyEvent): Boolean
+}
+
+@ExperimentalComposeUiApi
+internal class KeyInputInputModifierNodeImpl(
+    var onEvent: ((KeyEvent) -> Boolean)?,
+    var onPreEvent: ((KeyEvent) -> Boolean)?
+) : KeyInputModifierNode, Modifier.Node() {
+    override fun onKeyEvent(event: KeyEvent): Boolean = this.onEvent?.invoke(event) ?: false
+    override fun onPreKeyEvent(event: KeyEvent): Boolean = this.onPreEvent?.invoke(event) ?: false
+}
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/input/rotary/RotaryInputModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifier.kt
index 6b591e7..30002e0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifier.kt
@@ -18,50 +18,9 @@
 
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.node.DelegatableNode
 import androidx.compose.ui.node.modifierElementOf
 
 /**
- * Implement this interface to create a [Modifier.Node] that can intercept rotary scroll events.
- *
- * The event is routed to the focused item. Before reaching the focused item,
- * [onPreRotaryScrollEvent]() is called for parents of the focused item. If the parents don't
- * consume the event, [onPreRotaryScrollEvent]() is called for the focused item. If the event is
- * still not consumed, [onRotaryScrollEvent]() is called on the focused item's parents.
- */
-@ExperimentalComposeUiApi
-interface RotaryInputModifierNode : DelegatableNode {
-    /**
-     * This function is called when a [RotaryScrollEvent] is received by this node during the upward
-     * pass. While implementing this callback, return true to stop propagation of this event. If you
-     * return false, the key event will be sent to this [RotaryInputModifierNode]'s parent.
-     */
-    fun onRotaryScrollEvent(event: RotaryScrollEvent): Boolean
-
-    /**
-     * This function is called when a [RotaryScrollEvent] is received by this node during the
-     * downward pass. It gives ancestors of a focused component the chance to intercept an event.
-     * Return true to stop propagation of this event. If you return false, the event will be sent
-     * to this [RotaryInputModifierNode]'s child. If none of the children consume the event,
-     * it will be sent back up to the root using the [onRotaryScrollEvent] function.
-     */
-    fun onPreRotaryScrollEvent(event: RotaryScrollEvent): Boolean
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
-internal class RotaryInputModifierNodeImpl(
-    var onEvent: ((RotaryScrollEvent) -> Boolean)?,
-    var onPreEvent: ((RotaryScrollEvent) -> Boolean)?
-) : RotaryInputModifierNode, Modifier.Node() {
-    override fun onRotaryScrollEvent(event: RotaryScrollEvent): Boolean {
-        return onEvent?.invoke(event) ?: false
-    }
-    override fun onPreRotaryScrollEvent(event: RotaryScrollEvent): Boolean {
-        return onPreEvent?.invoke(event) ?: false
-    }
-}
-
-/**
  * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
  * allow it to intercept [RotaryScrollEvent]s if it (or one of its children) is focused.
  *
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifierNode.kt
new file mode 100644
index 0000000..c1be0a7
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifierNode.kt
@@ -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.compose.ui.input.rotary
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.DelegatableNode
+
+/**
+ * Implement this interface to create a [Modifier.Node] that can intercept rotary scroll events.
+ *
+ * The event is routed to the focused item. Before reaching the focused item,
+ * [onPreRotaryScrollEvent]() is called for parents of the focused item. If the parents don't
+ * consume the event, [onPreRotaryScrollEvent]() is called for the focused item. If the event is
+ * still not consumed, [onRotaryScrollEvent]() is called on the focused item's parents.
+ */
+@ExperimentalComposeUiApi
+interface RotaryInputModifierNode : DelegatableNode {
+    /**
+     * This function is called when a [RotaryScrollEvent] is received by this node during the upward
+     * pass. While implementing this callback, return true to stop propagation of this event. If you
+     * return false, the key event will be sent to this [RotaryInputModifierNode]'s parent.
+     */
+    fun onRotaryScrollEvent(event: RotaryScrollEvent): Boolean
+
+    /**
+     * This function is called when a [RotaryScrollEvent] is received by this node during the
+     * downward pass. It gives ancestors of a focused component the chance to intercept an event.
+     * Return true to stop propagation of this event. If you return false, the event will be sent
+     * to this [RotaryInputModifierNode]'s child. If none of the children consume the event,
+     * it will be sent back up to the root using the [onRotaryScrollEvent] function.
+     */
+    fun onPreRotaryScrollEvent(event: RotaryScrollEvent): Boolean
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal class RotaryInputModifierNodeImpl(
+    var onEvent: ((RotaryScrollEvent) -> Boolean)?,
+    var onPreEvent: ((RotaryScrollEvent) -> Boolean)?
+) : RotaryInputModifierNode, Modifier.Node() {
+    override fun onRotaryScrollEvent(event: RotaryScrollEvent): Boolean {
+        return onEvent?.invoke(event) ?: false
+    }
+    override fun onPreRotaryScrollEvent(event: RotaryScrollEvent): Boolean {
+        return onPreEvent?.invoke(event) ?: false
+    }
+}
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 49c7871..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
@@ -25,8 +25,8 @@
 import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.focus.FocusEventModifier
 import androidx.compose.ui.focus.FocusEventModifierNode
+import androidx.compose.ui.focus.FocusOrder
 import androidx.compose.ui.focus.FocusOrderModifier
-import androidx.compose.ui.focus.FocusOrderModifierToProperties
 import androidx.compose.ui.focus.FocusProperties
 import androidx.compose.ui.focus.FocusPropertiesModifierNode
 import androidx.compose.ui.focus.FocusRequesterModifier
@@ -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) {
@@ -451,3 +451,16 @@
 private val updateModifierLocalConsumer = { it: BackwardsCompatNode ->
     it.updateModifierLocalConsumer()
 }
+
+/**
+ * Used internally for FocusOrderModifiers so that we can compare the modifiers and can reuse
+ * the ModifierLocalConsumerEntity and ModifierLocalProviderEntity.
+ */
+@Suppress("DEPRECATION")
+private class FocusOrderModifierToProperties(
+    val modifier: FocusOrderModifier
+) : (FocusProperties) -> Unit {
+    override fun invoke(focusProperties: FocusProperties) {
+        modifier.populateFocusOrder(FocusOrder(focusProperties))
+    }
+}
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 d0bb5e4..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
@@ -27,7 +27,6 @@
 import androidx.compose.ui.focus.FocusProperties
 import androidx.compose.ui.focus.FocusPropertiesModifierNode
 import androidx.compose.ui.focus.FocusTargetModifierNode
-import androidx.compose.ui.focus.scheduleInvalidationOfAssociatedFocusTargets
 import androidx.compose.ui.input.key.KeyInputModifierNode
 import androidx.compose.ui.input.pointer.PointerInputModifier
 import androidx.compose.ui.input.rotary.RotaryInputModifierNode
@@ -206,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) {
@@ -246,6 +246,15 @@
     }
 }
 
+@ExperimentalComposeUiApi
+private fun FocusPropertiesModifierNode.scheduleInvalidationOfAssociatedFocusTargets() {
+    visitChildren(Nodes.FocusTarget) {
+        // Schedule invalidation for the focus target,
+        // which will cause it to recalculate focus properties.
+        requireOwner().focusOwner.scheduleInvalidation(it)
+    }
+}
+
 /**
  * This function checks if the FocusProperties node has set the canFocus [FocusProperties.canFocus]
  * property.
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/build.gradle b/constraintlayout/constraintlayout-compose-lint/build.gradle
new file mode 100644
index 0000000..31dce84
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose-lint/build.gradle
@@ -0,0 +1,45 @@
+/*
+ * 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.BundleInsideHelper
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("kotlin")
+}
+
+BundleInsideHelper.forInsideLintJar(project)
+
+dependencies {
+    compileOnly(libs.androidLintMinApi)
+    compileOnly(libs.kotlinStdlib)
+    bundleInside(project(":compose:lint:common"))
+
+    testImplementation(libs.kotlinStdlib)
+    testImplementation(libs.androidLint)
+    testImplementation(libs.androidLintTests)
+    testImplementation(libs.junit)
+    testImplementation(libs.truth)
+    testImplementation(project(":compose:lint:common-test"))
+}
+
+androidx {
+    name = "ConstraintLayout Compose lint checks"
+    type = LibraryType.LINT
+    inceptionYear = "2022"
+    description = "Lint checks for ConstraintLayout in Compose"
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose-lint/src/main/java/androidx/constraintlayout/compose/lint/ConstraintLayoutComposeIssueRegistry.kt b/constraintlayout/constraintlayout-compose-lint/src/main/java/androidx/constraintlayout/compose/lint/ConstraintLayoutComposeIssueRegistry.kt
new file mode 100644
index 0000000..13e6bed
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose-lint/src/main/java/androidx/constraintlayout/compose/lint/ConstraintLayoutComposeIssueRegistry.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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.constraintlayout.compose.lint
+
+import com.android.tools.lint.client.api.IssueRegistry
+import com.android.tools.lint.client.api.Vendor
+import com.android.tools.lint.detector.api.CURRENT_API
+
+private const val CL_COMPOSE_NEW_ISSUE = "new?component=323867&template=1023345"
+
+class ConstraintLayoutComposeIssueRegistry : IssueRegistry() {
+    override val api = 13
+
+    override val minApi = CURRENT_API
+
+    override val issues = listOf(
+        ConstraintLayoutDslDetector.IncorrectReferencesDeclarationIssue
+    )
+
+    override val vendor = Vendor(
+        feedbackUrl = "https://issuetracker.google.com/issues/$CL_COMPOSE_NEW_ISSUE",
+        identifier = "androidx.constraintlayout.compose",
+        vendorName = "Android Open Source Project",
+    )
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose-lint/src/main/java/androidx/constraintlayout/compose/lint/ConstraintLayoutDslDetector.kt b/constraintlayout/constraintlayout-compose-lint/src/main/java/androidx/constraintlayout/compose/lint/ConstraintLayoutDslDetector.kt
new file mode 100644
index 0000000..647017d
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose-lint/src/main/java/androidx/constraintlayout/compose/lint/ConstraintLayoutDslDetector.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.constraintlayout.compose.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import java.util.EnumSet
+import org.jetbrains.kotlin.psi.KtDestructuringDeclaration
+import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
+import org.jetbrains.uast.UCallExpression
+
+private const val CL_COMPOSE_PACKAGE = "androidx.constraintlayout.compose"
+private const val CONSTRAINT_SET_SCOPE_CLASS_FQ = "$CL_COMPOSE_PACKAGE.ConstraintSetScope"
+private const val MOTION_SCENE_SCOPE_CLASS_FQ = "$CL_COMPOSE_PACKAGE.MotionSceneScope"
+private const val CREATE_REFS_FOR_NAME = "createRefsFor"
+
+class ConstraintLayoutDslDetector : Detector(), SourceCodeScanner {
+    private val knownOwnersOfCreateRefsFor = setOf(
+        CONSTRAINT_SET_SCOPE_CLASS_FQ,
+        MOTION_SCENE_SCOPE_CLASS_FQ
+    )
+
+    override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)
+
+    override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
+        override fun visitCallExpression(node: UCallExpression) {
+            if (node.methodName != CREATE_REFS_FOR_NAME) {
+                return
+            }
+            val destructuringDeclarationElement =
+                node.sourcePsi?.getParentOfType<KtDestructuringDeclaration>(true) ?: return
+
+            val argsGiven = node.valueArgumentCount
+            val varsReceived = destructuringDeclarationElement.entries.size
+            if (argsGiven == varsReceived) {
+                // Ids provided to call match the variables assigned, no issue
+                return
+            }
+
+            // Verify that arguments are Strings, we can't check for correctness if the argument is
+            // an array: `val (text1, text2) = createRefsFor(*iDsArray)`
+            node.valueArguments.forEach { argExpression ->
+                if (argExpression.getExpressionType()?.canonicalText != String::class.java.name) {
+                    return
+                }
+            }
+
+            // Element resolution is relatively expensive, do last
+            val classOwnerFqName = node.resolve()?.containingClass?.qualifiedName ?: return
+
+            // Make sure the method corresponds to an expected class
+            if (!knownOwnersOfCreateRefsFor.contains(classOwnerFqName)) {
+                return
+            }
+
+            context.report(
+                issue = IncorrectReferencesDeclarationIssue,
+                scope = node,
+                location = context.getNameLocation(node),
+                message = "Arguments of `$CREATE_REFS_FOR_NAME` ($argsGiven) do not match " +
+                    "assigned variables ($varsReceived)"
+            )
+        }
+    }
+
+    companion object {
+        val IncorrectReferencesDeclarationIssue = Issue.create(
+            id = "IncorrectReferencesDeclaration",
+            briefDescription = "`$CREATE_REFS_FOR_NAME(vararg ids: Any)` should have at least one" +
+                " argument and match assigned variables",
+            explanation = "`$CREATE_REFS_FOR_NAME(vararg ids: Any)` conveniently allows creating " +
+                "multiple references using destructuring. However, providing an un-equal amount " +
+                "of arguments to the assigned variables will result in unexpected behavior since" +
+                " the variables may reference a ConstrainedLayoutReference with unknown ID.",
+            category = Category.CORRECTNESS,
+            priority = 5,
+            severity = Severity.ERROR,
+            implementation = Implementation(
+                ConstraintLayoutDslDetector::class.java,
+                EnumSet.of(Scope.JAVA_FILE)
+            )
+        )
+    }
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/constraintlayout/constraintlayout-compose-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
new file mode 100644
index 0000000..55db80e
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
@@ -0,0 +1 @@
+androidx.constraintlayout.compose.lint.ConstraintLayoutComposeIssueRegistry
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose-lint/src/test/java/androidx/constraintlayout/compose/lint/ApiLintVersionsTest.kt b/constraintlayout/constraintlayout-compose-lint/src/test/java/androidx/constraintlayout/compose/lint/ApiLintVersionsTest.kt
new file mode 100644
index 0000000..aa8c28f
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose-lint/src/test/java/androidx/constraintlayout/compose/lint/ApiLintVersionsTest.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.constraintlayout.compose.lint
+
+import com.android.tools.lint.client.api.LintClient
+import com.android.tools.lint.detector.api.CURRENT_API
+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 ApiLintVersionsTest {
+
+    @Test
+    fun versionsCheck() {
+        LintClient.clientName = LintClient.CLIENT_UNIT_TESTS
+
+        val registry = ConstraintLayoutComposeIssueRegistry()
+        assertThat(registry.api).isEqualTo(CURRENT_API)
+        assertThat(registry.minApi).isEqualTo(10)
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..359189a
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose-lint/src/test/java/androidx/constraintlayout/compose/lint/ConstraintLayoutDslDetectorTest.kt
@@ -0,0 +1,379 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.lint
+
+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
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+private const val COMPOSE_CONSTRAINTLAYOUT_FILE_PATH = "androidx/constraintlayout/compose"
+
+@RunWith(JUnit4::class)
+class ConstraintLayoutDslDetectorTest : LintDetectorTest() {
+    override fun getDetector() = ConstraintLayoutDslDetector()
+
+    override fun getIssues(): MutableList<Issue> =
+        mutableListOf(ConstraintLayoutDslDetector.IncorrectReferencesDeclarationIssue)
+
+    private val ConstraintSetScopeStub = bytecodeStub(
+        filename = "ConstraintSetScope.kt",
+        filepath = COMPOSE_CONSTRAINTLAYOUT_FILE_PATH,
+        checksum = 0x912b8878,
+        source = """
+            package androidx.constraintlayout.compose
+    
+            class ConstraintSetScope {
+                private var generatedCount = 0
+                private fun nextId() = "androidx.constraintlayout.id" + generatedCount++
+
+                fun createRefsFor(vararg ids: Any): ConstrainedLayoutReferences =
+                    ConstrainedLayoutReferences(arrayOf(*ids))
+                
+                inner class ConstrainedLayoutReferences internal constructor(
+                    private val ids: Array<Any>
+                ) {
+                    operator fun component1(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(0) { nextId() })
+                    operator fun component2(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(1) { nextId() })
+                    operator fun component3(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(2) { nextId() })
+                    operator fun component4(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(3) { nextId() })
+                    operator fun component5(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(4) { nextId() })
+                    operator fun component6(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(5) { nextId() })
+                    operator fun component7(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(6) { nextId() })
+                    operator fun component8(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(7) { nextId() })
+                    operator fun component9(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(8) { nextId() })
+                    operator fun component10(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(9) { nextId() })
+                    operator fun component11(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(10) { nextId() })
+                    operator fun component12(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(11) { nextId() })
+                    operator fun component13(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(12) { nextId() })
+                    operator fun component14(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(13) { nextId() })
+                    operator fun component15(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(14) { nextId() })
+                    operator fun component16(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(15) { nextId() })
+                }
+            }
+
+            class ConstrainedLayoutReference(val id: Any)
+        """.trimIndent(),
+        """
+                META-INF/main.kotlin_module:
+                H4sIAAAAAAAA/2NgYGBmYGBgBGJ2KM3AJcjFnlqRmFuQkyrEFpJaXOJdosSg
+                xQAASc3A6SsAAAA=
+                """,
+        """
+                androidx/constraintlayout/compose/ConstrainedLayoutReference.class:
+                H4sIAAAAAAAA/6VRXU8TURA9d/u1rEW2lUoBxQ9QSlEXiG8giWJMmhQ01PSl
+                T7e713Lb7S7ZvW3wrb/FX6CJRuODaXz0RxnnlhUFfPNlzpy5M2fmzvz4+fUb
+                gMd4yLDDAy8KpXfiuGEQq4jLQPn8bThQFOgfh7Fw9n4/CK8+eTkUb0QkAlfk
+                wBjsLh9yx+dBx3nZ7gpX5ZBiyO7IQKpdhlKlfjFhe63JsFwPo47TFaqtpWOH
+                B0GouJLUzDkI1cHA97cZDOmZMBmWeqHyZeB0h32HJhRRwH2nFqiISqUb52BR
+                J/dIuL2k9hWPeF9QIsPqPyb4K9LQIh2aKY88pi1cwVWGVEXzDGwLaRQYipcl
+                8jBxbQoGZhnS6kjGDLv1/9kmfTfTEarmMcxW1i43ZCjUkzXsC8U9rrjeUH+Y
+                omMybXIMrEehE6nZBnneJp14PMpbRtmwDHs8sgwzXR6PtowN9my6mLWNBfK+
+                v8saduqwcMZMazxaSJtpO6M1thjpo3Q2uWoI1XDDY/GopxgWDweBkn1RC4Yy
+                lm1fPP1zSdrMXugJhpk6ffhg0G+L6DWnHL3R0OV+k0dS8yS4clHr7IznRK1G
+                OIhc8ULqmvmkpnmpOzbpOmm9HBT1sQjvE8sS5ggNwgwxA6vEnhMahPZ6ceoL
+                ZqqfUayuf0LpwySzktRlsYE18q+f5hLOARPvVL+aXGHSoIAy5hN5BzoKZKof
+                UXp/ThOJZv40IdE8P+n6xN7DA8InFF2gvMUWUjXcqOEmWSxpc6uG27jTAotx
+                F8stZGPMxViJYcbaL8eY/wVNn9c9/QMAAA==
+                """,
+        """
+                androidx/constraintlayout/compose/ConstraintSetScope$ConstrainedLayoutReferences.class:
+                H4sIAAAAAAAA/62Y3VLbRhTH/ysbfwgDxjGO4xjqEIcYY2JsDCGE0pAEGoMh
+                FKc0Kf0StkIERs54RYbcMb3IG7QP0F70tp1pJpl2ppPhsi/Qt8n0yCggAco4
+                RAPePbva/Z/fnj2yVv737V//AChgi2FZUquNulLdzVbqKtcakqJqNel5fUej
+                ju2ndS5n7xxeKMtauVJ/KicPu+RqqTl4RX4sN2S1InMvGENwU3omZWuSupG9
+                v74pVzQvXAyeKUVVtGmGUqp0Frc310rHdW8OrjJEUjYXLpfqjY3spqyt6zo8
+                K6lqXZM0hZSzS3VtaadWu8ngUqrcBz9D31ZdqylqdvPZdpbcyg1VqmWLqtag
+                uUqFVtbO0FN5Ile2jMnLUkPalmkgw9XUSQRTT1kX2SCoADrQKSKALoqH9kTh
+                yRGG8bOFI4BuhPwQcI4WkdK129Ajwo0Iw7lTQhKAH1F9/AUGt+6bYeVMnt+3
+                /xRQsTlTlVUtx3ArNfghPk4Ikt5FY18q9VqN1tHcvplGQ3rOFyixPmEIbMha
+                SeJaUa3Ku3b5UAzgEvpFJHCZoXCWZXtxhaFDqtAqeVKVd7VilWH+jLk8eDI7
+                AriKlIgBDDJMfUzMvBiiTD0lIw9yZFhEBtcYFpJKUkoOU/DuN2ZrXB7+sJ1O
+                Hm1zkjaaFRkEhTYkas6AvINu8rqbI+lRB6VHrdIFB6ULVukxB6XHrNLjDkqP
+                W6WvOyh93So94aD0hFX6hoPSN3Tp9qOUp6/tkmP3z8gx8ZyT4rlj4nknxfPH
+                xEedFB89Jl5wUrxwTHzMSfGxY+LjToo3783ukvFUXJQ1qSppEj0ohe1nLjrY
+                Mb3w0jcyne+EXUVvUbYK1RxjvW/2YqIQFUQh+GZPpH8h2C0KPtdBn6/Lt//C
+                HX2zlxdG2O0e35u9UCAoxHwhd4g6Rlz7v3iEoHveH/TEhBHvvf0XAtk+k+03
+                2aLJbjfZAZPdYbI7TXaXyQ6a7G6THTLZ50x22GT3mOzIO3slYl7Tw/0f3bQu
+                N0WiTQ8S3R1gmP7oM8t7tpKezSf3/tqWRrNWdlRN2ZaL6jOFK+s1eeboxEpn
+                tjv1qszQVSLRpZ3tdbnxQKIxDKFSvSLVVqWGoreNTn9Z2VAlbadBdvK47uHJ
+                1eKgo6xJla1F6akhESiqqty4U5M416nFcn2nUZHnFP1a+KBxV17f2Zjd1WQ6
+                ItdVhguGp9UT/MjRydNNuSggpB9EKco/UMtDtQ8IBvXDLLW7qd1GvS5I1Fqj
+                0Xr+9mRC4msE00MvEU6/wvl05iVifzTF1qnspEEeiOhCO8JUV6gvcTARFxEH
+                mpbugDUt3b2AanO+F7Jxu+gcNKgXfdSte/+JJLxU59J/IlZyfUpVeCk+9BrJ
+                X+Efiud/Q2d83O0abxt+jTQmPX8j8+iC5xWyv9MkFx5TGYTwFjEvRpkXA/Ne
+                CoDOG6El6tS9FJQE1Un6XKHPO+4EjRuha200wos8WXrgcsZaTGEj3IINrttp
+                3KCBGyTcIOEGLbhjLeKO2+C2OY0bMnBDhBsi3JAF93qLuBM2uB6nccMGbphw
+                w4QbtuDeaBF30gbX6zRuxMCNEG6EcCMW3Jst4k7Z4Pqcxo0auFHCjRJu1IL7
+                aYu404e4Pxu4+SZu8OzZcPF03hh5ytH7aoxYY/RGGDvkvUTjPmvy9pl486fy
+                3rLjPXs62PDGDd448caJN27hnWmR97Yd79nzwYa3z+DtI94+4u2z8N5pkfeu
+                Ha/fad6EwZsg3gTxJiy8sy3yztnxik7z9hu8/cTbT7z9Ft7PW+S9Z8fb7jRv
+                0uBNEm+SeJMW3mKLvPN2vAGneQcM3gHiHWj+mXkXWuQt2fF2OM2bMnhTxJsi
+                2pSFd7FF3iU73k6nedMGb5p408SbtvDeb5F32Y63y2nejMGbId4M8WYsvF+0
+                xOvGBpUitQRS+I74n0A/IH8Pher/yoszy+Kp7y3iQvNNVEyXE++sOXEokUuc
+                PtrpX5zJVT7xWNpqip/2E/4cXVvQxHRJzF3OZQoTkzky8pOFG2J6VsQmLe4p
+                LXqFQlReg6uIB0V8SSVW9eKrIh7i0RoYx9dYW8N5jjjHNxz+ZunhiHL0cnzL
+                cZdjjuMexzxHiWOJY5mjwDHOMcExyTHFMc1xi+P2/ypvybWDGQAA
+                """,
+        """
+                androidx/constraintlayout/compose/ConstraintSetScope.class:
+                H4sIAAAAAAAA/7VUXU8bRxQ9s/5kMY5xAgGcpiTQxDaEdShN20DTglvKUgeo
+                qVAjXjrsTpyF9S7aWSN4Q+0v6F/oL2ilFqJGilAe+6Oq3jELKcbKA2pf7tw5
+                984583Hv/PX3n68BzKDOMMM9O/Ade9+wfE+GAXe80OUHfiskoLnrS2FUzwPr
+                Ily3/F2RAmPIbfM9brjcaxirW9vCClOIMSTnHM8JnzDEiqWNDBJI6ogjxRAP
+                XziS4VHtKoKzROyJ/dC0GW4US7W30uth4HgNit/sxBZajmuLIIU+HVm1g1tn
+                ylOdylOOnUaONPjurvBI40HxssRl1UhhNoM8riuRGwzZhvBEwENhV/2WFzIw
+                M4NB3OyBhiHaQ9F8N8+I4ikwpEP/NJjBe8go8DZDnxUIoq6L53LRDxgaxc1a
+                5yvQPq9yw+PnkLBr7WRSEYHwLCHpdsdqftAwtkW4pVKkwT3PD3no0CJjxQ9X
+                Wq5LWTHHlmmMMdze8UPX8YztvaZBIiLwuGuYnjqQdCyZwgcMA9YLYe1Ei9d4
+                wJuCEhnuFy+fqctrUHHdR1HHPZQY1v7rI6cwcVbhrdBxjfkg4AeEPqAiobUH
+                q88ZSt1u3yx1ATMwUNExhYcMteLVOqDbS7cb7EMdk5hhuN4lg0qGW3QcOX7W
+                PctXlO/Scxn0qsrU8ClDYvy0vftr0cs/FSG3echpC1pzL0bfDVOG2pDtELTv
+                qFmFPJvu5KeTw3u6NqTpWu7kUNfSyknryiVMDbn+CM3SdOjkcFqrsMcss5B4
+                80tSy2nLY7nESLKSrGsViuVzqRE9n0yzPGVV0nfJttN6ltM5fUSr9C5p9Wwu
+                Rl78+zc/ZlWMWNVWphltE/X/o4EK7whTL1zmm9qhzyNe9W3BcK1Gy1ZazS0R
+                fMe3XELyNd/i7gYPHDWPwEKdfhynKUxvz5EOQfNvu5RhvDN63nMX0jKmRx9Y
+                1eVSqp3p634rsMSiowSGI4qNS/R4SHUQV69MI3135MXJp++f7DLNDBrpdpEo
+                HyP9GzkaviGbbIMx1MhmThPQA53GvKquaPEykWk0jr5C9tkxruX7jzBQ/h3D
+                c+VC/Ic/MFw4wq0jvP9rB2/iX7yjEe/P5N0hRcVrUobiHZjIj79E+RUmn5Un
+                Jl6/xPQxPrpIlkS6TTZ4uiAiU94YHlH8aZR3l8aVqNDVJDeMj/FJl0t4fJGf
+                dVzCbJs/hlWyOmGTlLuEfqy1V5n4lsY64XOU+9kmYiaemPicLL5QZt7EAqqb
+                YBJf4qtN9EnoEosSSYnBtjMq8bXEWNu/I9Hbdpb+AeegSxAbCAAA
+                """
+    )
+
+    private val MotionSceneScopeStub = bytecodeStub(
+        filename = "MotionSceneScope.kt",
+        filepath = COMPOSE_CONSTRAINTLAYOUT_FILE_PATH,
+        checksum = 0xc89561d0,
+        source = """
+            package androidx.constraintlayout.compose
+
+            import androidx.constraintlayout.compose.ConstrainedLayoutReference
+
+            private const val UNDEFINED_NAME_PREFIX = "androidx.constraintlayout"
+
+            class MotionSceneScope {
+                /**
+                 * Count of generated ConstraintSet & Transition names.
+                 */
+                private var generatedCount = 0
+
+                /**
+                 * Count of generated ConstraintLayoutReference IDs.
+                 */
+                private var generatedIdCount = 0
+
+                private fun nextId() = UNDEFINED_NAME_PREFIX + "id" + generatedIdCount++
+
+                fun createRefsFor(vararg ids: Any): ConstrainedLayoutReferences =
+                    ConstrainedLayoutReferences(arrayOf(*ids))
+
+                inner class ConstrainedLayoutReferences internal constructor(
+                    private val ids: Array<Any>
+                ) {
+                    operator fun component1(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(0) { nextId() })
+                    operator fun component2(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(1) { nextId() })
+                    operator fun component3(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(2) { nextId() })
+                    operator fun component4(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(3) { nextId() })
+                    operator fun component5(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(4) { nextId() })
+                    operator fun component6(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(5) { nextId() })
+                    operator fun component7(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(6) { nextId() })
+                    operator fun component8(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(7) { nextId() })
+                    operator fun component9(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(8) { nextId() })
+                    operator fun component10(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(9) { nextId() })
+                    operator fun component11(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(10) { nextId() })
+                    operator fun component12(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(11) { nextId() })
+                    operator fun component13(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(12) { nextId() })
+                    operator fun component14(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(13) { nextId() })
+                    operator fun component15(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(14) { nextId() })
+                    operator fun component16(): ConstrainedLayoutReference =
+                        ConstrainedLayoutReference(ids.getOrElse(15) { nextId() })
+                }
+            }
+        """.trimIndent(),
+        """
+                META-INF/main.kotlin_module:
+                H4sIAAAAAAAA/2NgYGBmYGBgBGJ2KM3AZc6lmJiXUpSfmVKhl5yfV1xSlJiZ
+                V5KTWJlfWgIUyC3IL04VEvLNL8nMzwtOTs1LDU7OL0j1LuES5GJPrUjMLchJ
+                FWILSS0u8S5RYtBiAACRUOp0ZAAAAA==
+                """,
+        """
+                androidx/constraintlayout/compose/MotionSceneScope$ConstrainedLayoutReferences.class:
+                H4sIAAAAAAAA/62YW1PbRhTH/ysbX4QBQ4AQLgGKCRfjGN+4l4YQKAZDKE5p
+                UnoTtkIERma8giFvTB/yDdoP0D70tZ1pJpl2psPw2Ld+oUyPjAiSgzIO0YBX
+                Z9dn/+e3Z4/slf9989c/AJLYY1iT1HypqOSPo7miyrWSpKhaQXpePNRoYP+g
+                yOXoalFTimo2J6tyNlc8kEPzF55yPlN23ZCfyiVZzcncC8YQ3JWOpGhBUnei
+                D7d35ZzmhYvBM6OoijbLsDyY+fCg01uZStXpoU2G1kGbN/oyxdJOdFfWtnV5
+                HpVUtahJuiiPrhW1tcNCYZrBpeS5D36G23tFraCo0d2j/SjRyCVVKkTTqlai
+                uUqO1lXL0JJ7Juf2jMnrUknal8mRYWDwXQTTSFYX2SGoAOpQLyKABsqG9kzh
+                oVGG5HWSEUAjmvwQcIOWMKgr16BFhButDDeuSEgAfrTp/rcY3HpkhvVrxH3f
+                zlMyxfI8VVa1GMO9waEqItgLkl6HsSe5YqFAqyhv3VypJD3nK1RS3QyBHVnL
+                SFxLq3n52K4W0gH04hMRPehjiH/4or3oZ6iTcrRGHlLlYy2dZ1i6Vg0PvVsV
+                AQxgUMQdDDHMfEy+vAhThV5RiefVERExgrsM6ZASkkIRStzD0kKBy5EP2ePQ
+                5QaHaItZmkFQaCvazHsfdyxIXA9yKZxwTDhhFU46Jpy0CqccE05ZhcccEx6z
+                Co87JjxuFZ5wTHjCKjzpmPCkLlx7WeL04bzs0N0yWiEdc046ViEdd046XiGd
+                cE46USGddE46WSGdck46VSE95px0+T5szBjfeKuyJuUlTaIvQWH/yEWHNaY3
+                XvrMpTObcKzoPapPIR9jrOv0pF0U2gRRCJ6eiPQvBBtFwec6H/M1+M5euNtO
+                T+LCKLvf4js9aQoEhXZfk7uJBkZdZ794hKB72R/0tAuj3qWzFwLZPpPtN9mi
+                ya412QGTXWey6012g8kOmuxGk91ksm+Y7GaT3WKyWy/sjVbzmh6f/eimdbkp
+                EzV6kuieAMPsR59H3rOVdO6q3Pm7exrN2ThUNWVfTqtHCle2C/Lc5TmUzmLz
+                xbzM0JAhybXD/W259EgiH4amTDEnFTalkqL3jUF/VtlRJe2wRHaoUvftedQS
+                oC6rSbm9VenAkAikVVUuzRckznVmMVs8LOXkRUV/r/m880DePtxZONZkOvgW
+                VYZbRqTNd/gRoxOlmypRQJN+wKQc/0A9D119QDCoH1Kp30j9Ghp1QaLeFnnr
+                1dsy0iS+RnA4/BLNw69wc3jkJdr/KIttU1tPTh7cRAPJNtM1R2M95xPRgU6g
+                bOkBWNnSwwvIl+d7IRs3i85BTl24TcN69J9IwkvX2PCfaM+4PqVL81pn+DVC
+                v8If7oz/hvrOMbdrrCbyGsOY8vyNkSe3PK8Q/Z0mufCU2iCEN2j3IsG8uLPs
+                pQTovK20RBBZF2aIswMhevXT64K7h/xGKWE15OFFnCw9cTFjLaa0EW7SBtft
+                NG6XgdtFuF2E22XBTVWJO2aDW+M0breB20243YTbbcEdrxJ3wgbX4zRur4Hb
+                S7i9hNtrwZ2sEnfKBtfrNG6fgdtHuH2E22fBna4Sd8YG1+c0br+B20+4/eU/
+                M+6nVeLOvsX92cCNl3GD16+Gjqt5ByjSDNXAAJEO0PPewFveXvL7rMx728Qb
+                v5L3nh3v9cvBhnfI4B0i3iH9+dTCO1cl73073uvXgw1v2OANE2+YeMMW3vkq
+                eR/Y8fqd5o0YvBHijRBvxMK7UCXvoh2v6DRv1OCNEm+UeKMW3s+r5F2y4611
+                mjdm8MaIN0a8MQtvukreZTvegNO8CYM3QbwJ4k1YeFeq5M3Y8dY5zZsyeFPE
+                myLelIV3tUreNTveeqd5xw3eceIdJ95xC+/DKnnX7XgbnOadNHgniXeSeCct
+                vF9UxevGDrUi9QRS+I74n0E/IH8Pha7/ZVfn1sUrnlrElfJTqDic7bmwFsVw
+                T6znKl9nf0WmMPGep9JeWfqqn+QX6b0VTRzOiLG+2EhqcipGRnxqbFQcXhCx
+                S8s6oOVuUHKyW3Cl8SiNL6nFpt58lcZjPNkC4/gaW1u4ydHJ8Q2Hv9x6ONo4
+                uji+5XjAscixxLHMkeFY41jnSHKMcUxwTHHMcMxy3OO4/z+8pxUSTxkAAA==
+                """,
+        """
+                androidx/constraintlayout/compose/MotionSceneScope.class:
+                H4sIAAAAAAAA/61VW08bRxT+Zm1ssxjHOOHqNCWBNuYS1qH0FmhScEpZ11wK
+                LWrES4fdCVlY76KdNYI31H/Qp773F7RSC1EjRSiP/VFVzqyXpBgrUqLI0pkz
+                35z5vpkz56z//e+f5wBmsMEwzT078B370LB8T4YBd7zQ5Ud+IySgvu9LYSz7
+                oeN7G5bwxIbl74s0GEN+lx9ww+XejrG6vSusMI0EQ2rO8ZzwPkOiNLaZRQdS
+                OpJIMyTDJ45kmKm9vdws0XriMDRthmulsdpr4Y0wcLwdWu9vxRYajmuLII1u
+                HTmlXzzXnWrVdewM8iTB9/eFRxJ3SpcVLovGArNZFHBVaVyjlOzQkQMeCtu0
+                K37DCxmYmUUf+juhYYDhesl8M9OQYioyZEK/uZjFB8gq8AZDtxUIIl8Xj+Wi
+                HzCI0lat9RHopG+f4NHKeaSwa1EoaYhAeJaQlNyRmh/sGLsi3FYh0uCe54dc
+                UUhjxQ9XGq5LUQnHlhmMMNzY80PX8Yzdg7pB2iLwuGuYnrqOdCyZxkcMvdYT
+                Ye3Fm9d4wOuCAhluly7fqM1rUGXdRknHxxhjWHm/F05j4ry4qTZcYz4I+BGh
+                d6hEaO/R6mOGsXaZN8fagFkYKOuYwl2Gauldir/dG0ed9YmOScwwXG0TQcXC
+                LbqMHD1vnKV3Em/TbFl0qYrU8CVDx2izq3OvCj8u+55aXATLIuQ2DzmdSKsf
+                JOijw5ShhmR7BB06alYmz6YE/XZ2XNK1AU3X8mfHupZRTqY5EqigfE8M52g6
+                cHY8rZXZPda90PHi95SW16oj+dRQqpxe18q0VshnhvRCKsMKFFXuvEU2CtOr
+                mXzXkFbOLmnruXyCvORPL37JqTViJaijBVLHm2Z0dKy9/+4qvmGZHreVbWqP
+                0pus+LZguFKjTSuN+rYIfuDbLiGFmm9xd5MHjprHYHGd3sSpC9M7cKRD0Pzr
+                BmYYbV191Y4XwrKmR09ccbmU6lz6ht8ILLHoKIHBmGLzEj3uUqEk6Yk1+s2h
+                P/L6ojFJ6aS/B7Lf0cygkTKMjvFTZP6MwmpkUxGYxjLZbDMAndBpLKgyjDdX
+                iUyjcfgZco9OcaXQc4Le8b8wODdeTP78NwaLJ7h+gg//aOEt/I93OOb9lbyb
+                SES8Jqkp3t6JwuhTjD/D5KPxiYnnTzF9ik8vkqXQG5H1NTfEZMobwWe0vhLH
+                3aJxNW4ANckP4nN80SYJ9y7ys5YkzEb8CayR1QmbpFgTPfg+2lXFOo0/Ev4V
+                xd7fQsLEAxNfk8W8MgsmKni4BSbxDRa3kJPQJb6VSEnMRU6fxLDEksRINL0p
+                0RU55kuxHmQmPAgAAA==
+                """,
+        """
+                androidx/constraintlayout/compose/MotionSceneScopeKt.class:
+                H4sIAAAAAAAA/2WQTU8CMRCG3y7yISqC32DiQe+sGm6ejGKyEVYDSkw4mLI0
+                pLC0ZLdL9Eb8Kf4MD4Z49EcZp0ZjopfpzNN3Zt72/ePlFUANuww1rvqRlv0H
+                N9AqNhGXyoT8USeGwHiiY+E2tZFatQOhRDvQE3FpsmAMxSGfcjfkauBe9YYi
+                IJpi2Lz1z+sXnl8/v/dPm/X76xZVdwxrjV9520RSDU4Yyj/Lq3+X55BlOGjo
+                aOAOhenZm9jlSmnDrZnY9bXxkzCkIaXGSJtQKrcpDO9zw4k542mKXshsoEFs
+                ZBOH+IO02SFl/SOGvfksk5/P8k6xsF8ozmcV55DdvT3dvD1nHOJWdcxoDNb/
+                /kF1ZBhWzqxrrkyHh4lg2G0lysix8NRUxrIXitNfwwz5tk6iQFzIkKTlb2nn
+                nxBHcLBgzaOCNDJUbVvzyGGHzgzxReCLlL/iFulAXbSB9EtdpDwse1ihiIKH
+                VRQ9lLDWBYuxjo0unBjpGJufnYegJwYCAAA=
+                """
+    )
+
+    @Test
+    fun createRefsForArgumentTest() {
+        lint().files(
+            kotlin(
+                """
+                    package example
+                    
+                    import androidx.constraintlayout.compose.*
+                    
+                    fun Test() {
+                        val scopeApplier: ConstraintSetScope.() -> Unit = {
+                            val (box, text) = createRefsFor("box", "text")
+                            val (box1, text1, image1) = createRefsFor("box", "text")
+                            val (box2, text2) = createRefsFor("box", "text", "image")
+                    
+                            val ids = arrayOf("box", "text")
+                            val (box3, text3, image3) = createRefsFor(*ids)
+                        }                   
+                    }
+
+                    fun Test2() {
+                        val scopeApplier: MotionSceneScope.() -> Unit = {
+                            val (box, text) = createRefsFor("box", "text")
+                            val (box1, text1, image1) = createRefsFor("box", "text")
+                            val (box2, text2) = createRefsFor("box", "text", "image")
+                    
+                            val ids = arrayOf("box", "text")
+                            val (box3, text3, image3) = createRefsFor(*ids)
+                        }                   
+                    }
+                """.trimIndent()
+            ),
+            ConstraintSetScopeStub,
+            MotionSceneScopeStub
+        )
+            .run()
+            .expect(
+                """src/example/test.kt:8: Error: Arguments of createRefsFor (2) do not match assigned variables (3) [IncorrectReferencesDeclaration]
+        val (box1, text1, image1) = createRefsFor("box", "text")
+                                    ~~~~~~~~~~~~~
+src/example/test.kt:9: Error: Arguments of createRefsFor (3) do not match assigned variables (2) [IncorrectReferencesDeclaration]
+        val (box2, text2) = createRefsFor("box", "text", "image")
+                            ~~~~~~~~~~~~~
+src/example/test.kt:19: Error: Arguments of createRefsFor (2) do not match assigned variables (3) [IncorrectReferencesDeclaration]
+        val (box1, text1, image1) = createRefsFor("box", "text")
+                                    ~~~~~~~~~~~~~
+src/example/test.kt:20: Error: Arguments of createRefsFor (3) do not match assigned variables (2) [IncorrectReferencesDeclaration]
+        val (box2, text2) = createRefsFor("box", "text", "image")
+                            ~~~~~~~~~~~~~
+4 errors, 0 warnings"""
+            )
+    }
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/api/current.txt b/constraintlayout/constraintlayout-compose/api/current.txt
index edcf487..9b065fc 100644
--- a/constraintlayout/constraintlayout-compose/api/current.txt
+++ b/constraintlayout/constraintlayout-compose/api/current.txt
@@ -3,6 +3,7 @@
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface BaselineAnchorable {
     method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
+    method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
   }
 
   @androidx.compose.runtime.Immutable public final class ChainStyle {
@@ -32,6 +33,7 @@
     method public androidx.constraintlayout.compose.VerticalAnchorable getAbsoluteLeft();
     method public androidx.constraintlayout.compose.VerticalAnchorable getAbsoluteRight();
     method public float getAlpha();
+    method public androidx.constraintlayout.compose.Dimension getAsDimension(float);
     method public androidx.constraintlayout.compose.BaselineAnchorable getBaseline();
     method public androidx.constraintlayout.compose.HorizontalAnchorable getBottom();
     method public androidx.constraintlayout.compose.VerticalAnchorable getEnd();
@@ -132,13 +134,19 @@
     method public final androidx.constraintlayout.compose.HorizontalChainScope constrain(androidx.constraintlayout.compose.HorizontalChainReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.HorizontalChainScope,kotlin.Unit> constrainBlock);
     method public final androidx.constraintlayout.compose.VerticalChainScope constrain(androidx.constraintlayout.compose.VerticalChainReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.VerticalChainScope,kotlin.Unit> constrainBlock);
     method public final androidx.constraintlayout.compose.ConstrainScope constrain(androidx.constraintlayout.compose.ConstrainedLayoutReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstrainScope,kotlin.Unit> constrainBlock);
+    method public final void constrain(androidx.constraintlayout.compose.ConstrainedLayoutReference![] refs, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstrainScope,kotlin.Unit> constrainBlock);
     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);
@@ -152,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);
@@ -254,6 +264,26 @@
 
   @androidx.compose.foundation.layout.LayoutScopeMarker public final class ConstraintSetScope extends androidx.constraintlayout.compose.ConstraintLayoutBaseScope {
     method public androidx.constraintlayout.compose.ConstrainedLayoutReference createRefFor(Object id);
+    method public androidx.constraintlayout.compose.ConstraintSetScope.ConstrainedLayoutReferences createRefsFor(java.lang.Object... ids);
+  }
+
+  public final class ConstraintSetScope.ConstrainedLayoutReferences {
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component1();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component10();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component11();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component12();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component13();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component14();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component15();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component16();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component2();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component3();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component4();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component5();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component6();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component7();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component8();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component9();
   }
 
   public final class DesignElements {
@@ -324,6 +354,7 @@
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface HorizontalAnchorable {
     method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
+    method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
   }
 
   @androidx.compose.runtime.Stable public final class HorizontalChainReference extends androidx.constraintlayout.compose.LayoutReference {
@@ -387,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 9493fdfc..9a20684 100644
--- a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
@@ -8,10 +8,14 @@
   }
 
   public static final class Arc.Companion {
+    method public androidx.constraintlayout.compose.Arc getAbove();
+    method public androidx.constraintlayout.compose.Arc getBelow();
     method public androidx.constraintlayout.compose.Arc getFlip();
     method public androidx.constraintlayout.compose.Arc getNone();
     method public androidx.constraintlayout.compose.Arc getStartHorizontal();
     method public androidx.constraintlayout.compose.Arc getStartVertical();
+    property public final androidx.constraintlayout.compose.Arc Above;
+    property public final androidx.constraintlayout.compose.Arc Below;
     property public final androidx.constraintlayout.compose.Arc Flip;
     property public final androidx.constraintlayout.compose.Arc None;
     property public final androidx.constraintlayout.compose.Arc StartHorizontal;
@@ -34,6 +38,7 @@
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface BaselineAnchorable {
     method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
+    method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
   }
 
   @androidx.compose.runtime.Immutable public final class ChainStyle {
@@ -63,6 +68,7 @@
     method public androidx.constraintlayout.compose.VerticalAnchorable getAbsoluteLeft();
     method public androidx.constraintlayout.compose.VerticalAnchorable getAbsoluteRight();
     method public float getAlpha();
+    method public androidx.constraintlayout.compose.Dimension getAsDimension(float);
     method public androidx.constraintlayout.compose.BaselineAnchorable getBaseline();
     method public androidx.constraintlayout.compose.HorizontalAnchorable getBottom();
     method public androidx.constraintlayout.compose.VerticalAnchorable getEnd();
@@ -163,13 +169,19 @@
     method public final androidx.constraintlayout.compose.HorizontalChainScope constrain(androidx.constraintlayout.compose.HorizontalChainReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.HorizontalChainScope,kotlin.Unit> constrainBlock);
     method public final androidx.constraintlayout.compose.VerticalChainScope constrain(androidx.constraintlayout.compose.VerticalChainReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.VerticalChainScope,kotlin.Unit> constrainBlock);
     method public final androidx.constraintlayout.compose.ConstrainScope constrain(androidx.constraintlayout.compose.ConstrainedLayoutReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstrainScope,kotlin.Unit> constrainBlock);
+    method public final void constrain(androidx.constraintlayout.compose.ConstrainedLayoutReference![] refs, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstrainScope,kotlin.Unit> constrainBlock);
     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);
@@ -183,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);
@@ -285,6 +299,26 @@
 
   @androidx.compose.foundation.layout.LayoutScopeMarker public final class ConstraintSetScope extends androidx.constraintlayout.compose.ConstraintLayoutBaseScope {
     method public androidx.constraintlayout.compose.ConstrainedLayoutReference createRefFor(Object id);
+    method public androidx.constraintlayout.compose.ConstraintSetScope.ConstrainedLayoutReferences createRefsFor(java.lang.Object... ids);
+  }
+
+  public final class ConstraintSetScope.ConstrainedLayoutReferences {
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component1();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component10();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component11();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component12();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component13();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component14();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component15();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component16();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component2();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component3();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component4();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component5();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component6();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component7();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component8();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component9();
   }
 
   @androidx.constraintlayout.compose.ExperimentalMotionApi public final class CurveFit {
@@ -393,6 +427,7 @@
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface HorizontalAnchorable {
     method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
+    method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
   }
 
   @androidx.compose.runtime.Stable public final class HorizontalChainReference extends androidx.constraintlayout.compose.LayoutReference {
@@ -558,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);
@@ -569,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 {
@@ -594,17 +623,31 @@
     method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, float progress, optional androidx.compose.ui.Modifier modifier, optional String transitionName, optional java.util.EnumSet<androidx.constraintlayout.compose.MotionLayoutDebugFlags> debug, optional int optimizationLevel, optional java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag> motionLayoutFlags, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, optional androidx.compose.ui.Modifier modifier, optional String? constraintSetName, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional java.util.EnumSet<androidx.constraintlayout.compose.MotionLayoutDebugFlags> debug, optional int optimizationLevel, optional java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag> motionLayoutFlags, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, optional androidx.compose.ui.Modifier modifier, optional androidx.constraintlayout.compose.Transition? transition, float progress, optional java.util.EnumSet<androidx.constraintlayout.compose.MotionLayoutDebugFlags> debug, optional androidx.constraintlayout.compose.LayoutInformationReceiver? informationReceiver, optional int optimizationLevel, optional java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag> motionLayoutFlags, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(optional androidx.compose.ui.Modifier modifier, optional int optimizationLevel, androidx.constraintlayout.compose.MotionLayoutState motionLayoutState, androidx.constraintlayout.compose.MotionScene motionScene, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, androidx.constraintlayout.compose.MotionLayoutState motionLayoutState, optional androidx.compose.ui.Modifier modifier, optional int optimizationLevel, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
   }
 
   @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.constraintlayout.compose.ExperimentalMotionApi public final class MotionLayoutScope {
-    method public long motionColor(String id, String name);
-    method public float motionDistance(String id, String name);
-    method public float motionFloat(String id, String name);
-    method public long motionFontSize(String id, String name);
-    method public int motionInt(String id, String name);
-    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.constraintlayout.compose.MotionLayoutScope.MotionProperties> motionProperties(String id);
-    method public androidx.constraintlayout.compose.MotionLayoutScope.MotionProperties motionProperties(String id, String tag);
+    method public long customColor(String id, String name);
+    method public float customDistance(String id, String name);
+    method public float customFloat(String id, String name);
+    method public long customFontSize(String id, String name);
+    method public int customInt(String id, String name);
+    method public androidx.constraintlayout.compose.MotionLayoutScope.CustomProperties customProperties(String id);
+    method @Deprecated public long motionColor(String id, String name);
+    method @Deprecated public float motionDistance(String id, String name);
+    method @Deprecated public float motionFloat(String id, String name);
+    method @Deprecated public long motionFontSize(String id, String name);
+    method @Deprecated public int motionInt(String id, String name);
+    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.constraintlayout.compose.MotionLayoutScope.MotionProperties> motionProperties(String id);
+    method @Deprecated public androidx.constraintlayout.compose.MotionLayoutScope.MotionProperties motionProperties(String id, String tag);
+  }
+
+  @androidx.constraintlayout.compose.ExperimentalMotionApi public final class MotionLayoutScope.CustomProperties {
+    method public long color(String name);
+    method public float distance(String name);
+    method public float float(String name);
+    method public long fontSize(String name);
+    method public int int(String name);
   }
 
   @androidx.constraintlayout.compose.ExperimentalMotionApi public final class MotionLayoutScope.MotionProperties {
@@ -654,6 +697,7 @@
     method public void addTransition(androidx.constraintlayout.compose.Transition transition, optional String? name);
     method public androidx.constraintlayout.compose.ConstraintSetRef constraintSet(optional String? name, optional androidx.constraintlayout.compose.ConstraintSetRef? extendConstraintSet, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstraintSetScope,kotlin.Unit> constraintSetContent);
     method public androidx.constraintlayout.compose.ConstrainedLayoutReference createRefFor(Object id);
+    method public androidx.constraintlayout.compose.MotionSceneScope.ConstrainedLayoutReferences createRefsFor(java.lang.Object... ids);
     method public void customColor(androidx.constraintlayout.compose.ConstrainScope, String name, long value);
     method public void customColor(androidx.constraintlayout.compose.KeyAttributeScope, String name, long value);
     method public void customDistance(androidx.constraintlayout.compose.ConstrainScope, String name, float value);
@@ -668,14 +712,27 @@
     method public void transition(androidx.constraintlayout.compose.ConstraintSetRef from, androidx.constraintlayout.compose.ConstraintSetRef to, optional String? name, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.TransitionScope,kotlin.Unit> transitionContent);
   }
 
-  public final class MotionSceneScopeKt {
-    method @androidx.constraintlayout.compose.ExperimentalMotionApi public static androidx.constraintlayout.compose.MotionScene MotionScene(kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionSceneScope,kotlin.Unit> motionSceneContent);
+  public final class MotionSceneScope.ConstrainedLayoutReferences {
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component1();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component10();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component11();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component12();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component13();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component14();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component15();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component16();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component2();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component3();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component4();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component5();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component6();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component7();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component8();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component9();
   }
 
-  @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);
+  public final class MotionSceneScopeKt {
+    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 public final class OnSwipe {
diff --git a/constraintlayout/constraintlayout-compose/api/restricted_current.txt b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
index b18e252..a52b8ea 100644
--- a/constraintlayout/constraintlayout-compose/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
@@ -3,6 +3,7 @@
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface BaselineAnchorable {
     method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
+    method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
   }
 
   @androidx.compose.runtime.Immutable public final class ChainStyle {
@@ -19,6 +20,13 @@
     property public final androidx.constraintlayout.compose.ChainStyle SpreadInside;
   }
 
+  @kotlin.PublishedApi internal enum CompositionSource {
+    method public static androidx.constraintlayout.compose.CompositionSource valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.constraintlayout.compose.CompositionSource[] values();
+    enum_constant public static final androidx.constraintlayout.compose.CompositionSource Content;
+    enum_constant public static final androidx.constraintlayout.compose.CompositionSource Unknown;
+  }
+
   @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Stable public final class ConstrainScope {
     method public void centerAround(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.VerticalAnchor anchor);
     method public void centerAround(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor);
@@ -32,6 +40,7 @@
     method public androidx.constraintlayout.compose.VerticalAnchorable getAbsoluteLeft();
     method public androidx.constraintlayout.compose.VerticalAnchorable getAbsoluteRight();
     method public float getAlpha();
+    method public androidx.constraintlayout.compose.Dimension getAsDimension(float);
     method public androidx.constraintlayout.compose.BaselineAnchorable getBaseline();
     method public androidx.constraintlayout.compose.HorizontalAnchorable getBottom();
     method public androidx.constraintlayout.compose.VerticalAnchorable getEnd();
@@ -132,13 +141,19 @@
     method public final androidx.constraintlayout.compose.HorizontalChainScope constrain(androidx.constraintlayout.compose.HorizontalChainReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.HorizontalChainScope,kotlin.Unit> constrainBlock);
     method public final androidx.constraintlayout.compose.VerticalChainScope constrain(androidx.constraintlayout.compose.VerticalChainReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.VerticalChainScope,kotlin.Unit> constrainBlock);
     method public final androidx.constraintlayout.compose.ConstrainScope constrain(androidx.constraintlayout.compose.ConstrainedLayoutReference ref, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstrainScope,kotlin.Unit> constrainBlock);
+    method public final void constrain(androidx.constraintlayout.compose.ConstrainedLayoutReference![] refs, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstrainScope,kotlin.Unit> constrainBlock);
     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);
@@ -152,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);
@@ -202,8 +219,6 @@
     method public static androidx.constraintlayout.compose.Dimension getAtLeastWrapContent(androidx.constraintlayout.compose.Dimension.MinCoercible);
     method public static androidx.constraintlayout.compose.Dimension.MinCoercible getAtMostWrapContent(androidx.constraintlayout.compose.Dimension.Coercible);
     method public static androidx.constraintlayout.compose.Dimension getAtMostWrapContent(androidx.constraintlayout.compose.Dimension.MaxCoercible);
-    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static kotlin.Pair<androidx.compose.ui.layout.MeasurePolicy,kotlin.jvm.functions.Function0<kotlin.Unit>> rememberConstraintLayoutMeasurePolicy(int optimizationLevel, androidx.constraintlayout.compose.ConstraintLayoutScope scope, androidx.compose.runtime.MutableState<java.lang.Boolean> remeasureRequesterState, androidx.constraintlayout.compose.Measurer measurer);
-    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.layout.MeasurePolicy rememberConstraintLayoutMeasurePolicy(int optimizationLevel, androidx.compose.runtime.MutableState<java.lang.Long> needsUpdate, androidx.constraintlayout.compose.ConstraintSet constraintSet, androidx.constraintlayout.compose.Measurer measurer);
   }
 
   @androidx.compose.foundation.layout.LayoutScopeMarker public final class ConstraintLayoutScope extends androidx.constraintlayout.compose.ConstraintLayoutBaseScope {
@@ -252,12 +267,45 @@
     method public default androidx.constraintlayout.compose.ConstraintSet override(String name, float value);
   }
 
+  @kotlin.PublishedApi internal final class ConstraintSetForInlineDsl implements androidx.constraintlayout.compose.ConstraintSet androidx.compose.runtime.RememberObserver {
+    ctor public ConstraintSetForInlineDsl(androidx.constraintlayout.compose.ConstraintLayoutScope scope);
+    method public void applyTo(androidx.constraintlayout.compose.State state, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables);
+    method public boolean getKnownDirty();
+    method public androidx.constraintlayout.compose.ConstraintLayoutScope getScope();
+    method public void onAbandoned();
+    method public void onForgotten();
+    method public void onRemembered();
+    method public void setKnownDirty(boolean);
+    property public final boolean knownDirty;
+    property public final androidx.constraintlayout.compose.ConstraintLayoutScope scope;
+  }
+
   public final class ConstraintSetRef {
     method public androidx.constraintlayout.compose.ConstraintSetRef copy(String name);
   }
 
   @androidx.compose.foundation.layout.LayoutScopeMarker public final class ConstraintSetScope extends androidx.constraintlayout.compose.ConstraintLayoutBaseScope {
     method public androidx.constraintlayout.compose.ConstrainedLayoutReference createRefFor(Object id);
+    method public androidx.constraintlayout.compose.ConstraintSetScope.ConstrainedLayoutReferences createRefsFor(java.lang.Object... ids);
+  }
+
+  public final class ConstraintSetScope.ConstrainedLayoutReferences {
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component1();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component10();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component11();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component12();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component13();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component14();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component15();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component16();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component2();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component3();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component4();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component5();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component6();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component7();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component8();
+    method public operator androidx.constraintlayout.compose.ConstrainedLayoutReference component9();
   }
 
   public final class DesignElements {
@@ -349,6 +397,7 @@
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface HorizontalAnchorable {
     method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.HorizontalAnchor anchor, optional float margin, optional float goneMargin);
+    method public void linkTo(androidx.constraintlayout.compose.ConstraintLayoutBaseScope.BaselineAnchor anchor, optional float margin, optional float goneMargin);
   }
 
   @androidx.compose.runtime.Stable public final class HorizontalChainReference extends androidx.constraintlayout.compose.LayoutReference {
@@ -461,7 +510,7 @@
   }
 
   public final class MotionDragHandlerKt {
-    method @kotlin.PublishedApi internal static inline androidx.compose.ui.Modifier motionPointerInput(androidx.compose.ui.Modifier, optional Object key, androidx.constraintlayout.compose.MotionProgress motionProgress, androidx.constraintlayout.compose.MotionMeasurer measurer);
+    method @kotlin.PublishedApi internal static inline androidx.compose.ui.Modifier motionPointerInput(androidx.compose.ui.Modifier, Object key, androidx.constraintlayout.compose.MotionProgress motionProgress, androidx.constraintlayout.compose.MotionMeasurer measurer);
   }
 
   @kotlin.PublishedApi internal final class MotionDragState {
@@ -486,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);
@@ -516,12 +562,13 @@
 
   public final class MotionLayoutKt {
     method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static inline void MotionLayoutCore(androidx.constraintlayout.compose.MotionScene motionScene, optional androidx.compose.ui.Modifier modifier, optional String? constraintSetName, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional androidx.constraintlayout.compose.MotionLayoutDebugFlags debugFlag, optional int optimizationLevel, optional java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag> motionLayoutFlags, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static inline void MotionLayoutCore(androidx.constraintlayout.compose.MotionScene motionScene, float progress, optional androidx.compose.ui.Modifier modifier, optional java.util.EnumSet<androidx.constraintlayout.compose.MotionLayoutDebugFlags> debug, optional int optimizationLevel, String transitionName, optional java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag> motionLayoutFlags, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static inline void MotionLayoutCore(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, optional androidx.compose.ui.Modifier modifier, optional androidx.constraintlayout.compose.TransitionImpl? transition, androidx.constraintlayout.compose.MotionProgress motionProgress, optional androidx.constraintlayout.compose.MotionLayoutDebugFlags debugFlag, optional androidx.constraintlayout.compose.LayoutInformationReceiver? informationReceiver, optional int optimizationLevel, optional java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag> motionLayoutFlags, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static inline void MotionLayoutCore(optional androidx.compose.ui.Modifier modifier, optional int optimizationLevel, androidx.constraintlayout.compose.MotionLayoutStateImpl motionLayoutState, androidx.constraintlayout.compose.MotionScene motionScene, optional String transitionName, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static inline void MotionLayoutCore(androidx.constraintlayout.compose.MotionScene motionScene, float progress, String transitionName, int optimizationLevel, java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag> motionLayoutFlags, java.util.EnumSet<androidx.constraintlayout.compose.MotionLayoutDebugFlags> debug, androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static inline void MotionLayoutCore(androidx.constraintlayout.compose.MotionScene motionScene, String transitionName, androidx.constraintlayout.compose.MotionLayoutStateImpl motionLayoutState, int optimizationLevel, androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static inline void MotionLayoutCore(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, androidx.constraintlayout.compose.TransitionImpl? transition, androidx.constraintlayout.compose.MotionProgress motionProgress, androidx.constraintlayout.compose.LayoutInformationReceiver? informationReceiver, int optimizationLevel, boolean showBounds, boolean showPaths, boolean showKeyPositions, androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static void UpdateWithForcedIfNoUserChange(androidx.constraintlayout.compose.MotionProgress motionProgress, androidx.constraintlayout.compose.LayoutInformationReceiver? informationReceiver);
-    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static inline androidx.constraintlayout.compose.MotionProgress createAndUpdateMotionProgress(float progress);
-    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.layout.MeasurePolicy rememberMotionLayoutMeasurePolicy(int optimizationLevel, java.util.EnumSet<androidx.constraintlayout.compose.MotionLayoutDebugFlags> debug, androidx.constraintlayout.compose.ConstraintSet constraintSetStart, androidx.constraintlayout.compose.ConstraintSet constraintSetEnd, androidx.constraintlayout.compose.TransitionImpl? transition, androidx.constraintlayout.compose.MotionProgress motionProgress, optional java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag> motionLayoutFlags, androidx.constraintlayout.compose.MotionMeasurer measurer);
+    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.constraintlayout.compose.MotionProgress createAndUpdateMotionProgress(float progress);
+    method @kotlin.PublishedApi internal static androidx.compose.ui.Modifier motionDebug(androidx.compose.ui.Modifier, androidx.constraintlayout.compose.MotionMeasurer measurer, float scaleFactor, boolean showBounds, boolean showPaths, boolean showKeyPositions);
+    method @kotlin.PublishedApi internal static androidx.compose.ui.layout.MeasurePolicy motionLayoutMeasurePolicy(androidx.compose.runtime.State<kotlin.Unit> contentTracker, androidx.compose.ui.node.Ref<androidx.constraintlayout.compose.CompositionSource> compositionSource, androidx.constraintlayout.compose.ConstraintSet constraintSetStart, androidx.constraintlayout.compose.ConstraintSet constraintSetEnd, androidx.constraintlayout.compose.TransitionImpl transition, androidx.constraintlayout.compose.MotionProgress motionProgress, androidx.constraintlayout.compose.MotionMeasurer measurer, int optimizationLevel);
   }
 
   @androidx.compose.runtime.Immutable @kotlin.PublishedApi internal final class MotionLayoutStateImpl {
@@ -543,13 +590,13 @@
   @kotlin.PublishedApi internal final class MotionMeasurer extends androidx.constraintlayout.compose.Measurer {
     ctor public MotionMeasurer(androidx.compose.ui.unit.Density density);
     method public void clearConstraintSets();
-    method public void drawDebug(androidx.compose.ui.graphics.drawscope.DrawScope);
+    method public void drawDebug(androidx.compose.ui.graphics.drawscope.DrawScope, optional boolean drawBounds, optional boolean drawPaths, optional boolean drawKeyPositions);
     method public void encodeRoot(StringBuilder json);
     method public long getCustomColor(String id, String name, float progress);
     method public float getCustomFloat(String id, String name, float progress);
     method public androidx.constraintlayout.core.state.Transition getTransition();
-    method public void initWith(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, androidx.compose.ui.unit.Density density, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.constraintlayout.compose.TransitionImpl? transition, float progress);
-    method public long performInterpolationMeasure(long constraints, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.constraintlayout.compose.ConstraintSet constraintSetStart, androidx.constraintlayout.compose.ConstraintSet constraintSetEnd, androidx.constraintlayout.compose.TransitionImpl? transition, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables, int optimizationLevel, float progress, optional java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag> motionLayoutFlags);
+    method public void initWith(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.constraintlayout.compose.TransitionImpl transition, float progress);
+    method public long performInterpolationMeasure(long constraints, androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.constraintlayout.compose.ConstraintSet constraintSetStart, androidx.constraintlayout.compose.ConstraintSet constraintSetEnd, androidx.constraintlayout.compose.TransitionImpl transition, java.util.List<? extends androidx.compose.ui.layout.Measurable> measurables, int optimizationLevel, float progress, androidx.constraintlayout.compose.CompositionSource compositionSource);
     property public final androidx.constraintlayout.core.state.Transition transition;
   }
 
@@ -597,6 +644,18 @@
     method public suspend Object? updateProgressWhileTouchUp(kotlin.coroutines.Continuation<? super kotlin.Unit>);
   }
 
+  @kotlin.PublishedApi internal final class TransitionImpl {
+    ctor public TransitionImpl(androidx.constraintlayout.core.parser.CLObject parsedTransition);
+    method public void applyAllTo(androidx.constraintlayout.core.state.Transition transition);
+    method public void applyKeyFramesTo(androidx.constraintlayout.core.state.Transition transition);
+    method public String getEndConstraintSetId();
+    method public String getStartConstraintSetId();
+    field @kotlin.PublishedApi internal static final androidx.constraintlayout.compose.TransitionImpl EMPTY;
+  }
+
+  @kotlin.PublishedApi internal static final class TransitionImpl.Companion {
+  }
+
   public final class TransitionKt {
   }
 
diff --git a/constraintlayout/constraintlayout-compose/build.gradle b/constraintlayout/constraintlayout-compose/build.gradle
index 2d46d36..bc8b31b 100644
--- a/constraintlayout/constraintlayout-compose/build.gradle
+++ b/constraintlayout/constraintlayout-compose/build.gradle
@@ -46,6 +46,8 @@
         androidTestImplementation(libs.testRules)
         androidTestImplementation(libs.testRunner)
         androidTestImplementation(libs.junit)
+
+        lintPublish(project(":constraintlayout:constraintlayout-compose-lint"))
     }
 }
 
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/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/CustomKeyAttributesDemo.kt
deleted file mode 100644
index fc0af6f..0000000
--- a/constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/CustomKeyAttributesDemo.kt
+++ /dev/null
@@ -1,108 +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.
- */
-
-@file:OptIn(ExperimentalMotionApi::class)
-
-package androidx.constraintlayout.compose.demos
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.layout.layoutId
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.constraintlayout.compose.Dimension
-import androidx.constraintlayout.compose.ExperimentalMotionApi
-import androidx.constraintlayout.compose.MotionLayout
-import androidx.constraintlayout.compose.MotionLayoutFlag
-import androidx.constraintlayout.compose.MotionScene
-import androidx.constraintlayout.compose.OnSwipe
-import androidx.constraintlayout.compose.SwipeDirection
-import androidx.constraintlayout.compose.SwipeSide
-
-@Preview
-@Composable
-fun CustomColorInKeyAttributesDemo() {
-    val boxId = "box"
-    val textId = "text"
-    MotionLayout(
-        motionScene = MotionScene {
-            val box = createRefFor(boxId)
-            val text = createRefFor(textId)
-            defaultTransition(
-                from = constraintSet {
-                    constrain(text) {
-                        top.linkTo(box.bottom, 10.dp)
-                        start.linkTo(box.start)
-                    }
-                    constrain(box) {
-                        width = Dimension.value(50.dp)
-                        height = Dimension.value(50.dp)
-
-                        centerVerticallyTo(parent)
-                        start.linkTo(parent.start)
-
-                        customColor("background", Color.Red)
-                    }
-                },
-                to = constraintSet {
-                    constrain(text) {
-                        top.linkTo(box.bottom, 10.dp)
-                        end.linkTo(box.end)
-                    }
-                    constrain(box) {
-                        width = Dimension.value(50.dp)
-                        height = Dimension.value(50.dp)
-
-                        centerVerticallyTo(parent)
-                        end.linkTo(parent.end)
-
-                        customColor("background", Color.Blue)
-                    }
-                }
-            ) {
-                onSwipe = OnSwipe(
-                    anchor = box,
-                    side = SwipeSide.Middle,
-                    direction = SwipeDirection.End
-                )
-                keyAttributes(box) {
-                    frame(33) {
-                        customColor("background", Color.Yellow)
-                    }
-                    frame(66) {
-                        customColor("background", Color.Green)
-                    }
-                }
-            }
-        },
-        progress = 0f,
-        motionLayoutFlags = setOf(MotionLayoutFlag.FullMeasure), // Needed to update text composable
-        modifier = Modifier.fillMaxSize()
-    ) {
-        val background = motionColor(boxId, "background")
-        Box(modifier = Modifier.layoutId(boxId).background(background))
-        Text(
-            modifier = Modifier.layoutId(textId),
-            text = "Color: ${background.toArgb().toUInt().toString(16)}"
-        )
-    }
-}
\ No newline at end of file
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/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/OnSwipeDemos.kt
deleted file mode 100644
index 59b84d1..0000000
--- a/constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/OnSwipeDemos.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.
- */
-
-@file:OptIn(ExperimentalMotionApi::class)
-
-package androidx.constraintlayout.compose.demos
-
-import androidx.compose.animation.core.tween
-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.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.material.Button
-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.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.constraintlayout.compose.ExperimentalMotionApi
-import androidx.constraintlayout.compose.MotionLayout
-import androidx.constraintlayout.compose.MotionLayoutDebugFlags
-import androidx.constraintlayout.compose.MotionScene
-import androidx.constraintlayout.compose.layoutId
-import androidx.constraintlayout.compose.rememberMotionLayoutState
-
-@Preview
-@Composable
-fun SimpleOnSwipe() {
-    var mode by remember {
-        mutableStateOf("spring")
-    }
-    var toEnd by remember { mutableStateOf(true) }
-    val motionLayoutState = rememberMotionLayoutState(key = mode)
-
-    val motionSceneContent = remember(mode) {
-        // language=json5
-        """
-       {
-         Header: { exportAs: 'swipeExpr' },
-         ConstraintSets: {
-           start: {
-             box: {
-               width: 50, height: 50,
-               bottom: ['parent', 'bottom', 10],
-               start: ['parent', 'start', 10],
-               custom: {
-                 bColor: '#ff0000'
-               }
-             }
-           },
-           end: {
-             Extends: 'start',
-             box: {
-               clear: ['constraints'],
-                  width: 100, height: 400,
-               top: ['parent', 'top', 10],
-               end: ['parent', 'end', 10],
-               custom: {
-                 bColor: '#0000ff'
-               }
-             }
-           }
-         },
-         Transitions: {
-           default: {
-              from: 'start',
-              to: 'end',
-              KeyFrames: {
-                KeyPositions: [{
-                  target: ['box'],
-                  frames: [25, 50, 75],
-                  percentX: [0.25, 0.5, 0.75],
-                  percentY: [0.25, 0.5, 0.75]
-                }],
-              },
-              onSwipe: {
-                anchor: 'box',
-                direction: 'end',
-                side: 'start',
-                mode: '$mode',
-                springMass: 1,
-                springDamping: 10,
-                springStiffness: 70,
-              }
-           }
-         }
-       }
-        """.trimIndent()
-    }
-    Column {
-        Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
-            Button(onClick = { motionLayoutState.snapTo(0f) }) {
-                Text(text = "Reset")
-            }
-            Button(onClick = {
-                val target = if (toEnd) 1f else 0f
-                motionLayoutState.animateTo(target, tween(2000))
-                toEnd = !toEnd
-            }) {
-                Text(text = if (toEnd) "End" else "Start")
-            }
-            Button(onClick = {
-                if (motionLayoutState.isInDebugMode) {
-                    motionLayoutState.setDebugMode(MotionLayoutDebugFlags.NONE)
-                } else {
-                    motionLayoutState.setDebugMode(MotionLayoutDebugFlags.SHOW_ALL)
-                }
-            }) {
-                Text("Debug")
-            }
-            Button(onClick = {
-                mode = when (mode) {
-                    "spring" -> "velocity"
-                    else -> "spring"
-                }
-            }) {
-                Text(text = "Mode: $mode")
-            }
-        }
-        MotionLayout(
-            modifier = Modifier
-                .fillMaxWidth()
-                .weight(1.0f, fill = true),
-            motionLayoutState = motionLayoutState,
-            motionScene = MotionScene(content = motionSceneContent)
-        ) {
-            Box(
-                modifier = Modifier
-                    .background(motionProperties(id = "box").value.color("bColor"))
-                    .layoutId("box")
-            )
-        }
-        Text(text = "Current progress: ${motionLayoutState.currentProgress}")
-    }
-}
\ No newline at end of file
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/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
new file mode 100644
index 0000000..20dacf6
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/integration-tests/demos/src/main/java/androidx/constraintlayout/compose/demos/AllDemos.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.demos
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+data class ComposeDemo(val title: String, val content: @Composable () -> Unit)
+
+val AllComposeConstraintLayoutDemos: List<ComposeDemo> =
+    listOf(
+        ComposeDemo("CustomColorInKeyAttributes") { CustomColorInKeyAttributesDemo() },
+        ComposeDemo("SimpleOnSwipe") { SimpleOnSwipe() },
+        ComposeDemo("AnimatedChainOrientation") { ChainsAnimatedOrientationDemo() }
+    )
+
+/**
+ * Main screen to explore and interact with all demos from [AllComposeConstraintLayoutDemos].
+ */
+@Preview
+@Composable
+fun ComposeConstraintLayoutDemos() {
+    var displayedDemo by remember { mutableStateOf<ComposeDemo?>(null) }
+    Column {
+        Column {
+            displayedDemo?.let {
+                // Header with back button
+                Row(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .height(50.dp)
+                        .background(Color.White)
+                        .graphicsLayer(shadowElevation = 2f)
+                        .clickable { displayedDemo = null }, // Return to list of demos
+                    verticalAlignment = Alignment.CenterVertically
+                ) {
+                    Icon(imageVector = Icons.Default.ArrowBack, contentDescription = "Back")
+                    Text(text = it.title)
+                }
+            } ?: kotlin.run {
+                // Main Title
+                Text(text = "ComposeConstraintLayoutDemos", style = MaterialTheme.typography.h6)
+                Spacer(modifier = Modifier.height(8.dp))
+            }
+        }
+        Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+            displayedDemo?.let { demo ->
+                // Display selected demo
+                Box(
+                    Modifier
+                        .fillMaxWidth()
+                        .weight(1.0f, true)
+                ) {
+                    demo.content()
+                }
+            } ?: kotlin.run {
+                // Display list of demos
+                AllComposeConstraintLayoutDemos.forEach {
+                    ComposeDemoItem(it.title) { displayedDemo = it }
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun ComposeDemoItem(title: String, modifier: Modifier = Modifier, onClick: () -> Unit) {
+    Box(
+        modifier = modifier
+            .padding(horizontal = 8.dp)
+            .fillMaxWidth()
+            .height(44.dp)
+            .clip(RoundedCornerShape(10.dp))
+            .background(Color.White)
+            .clickable(onClick = onClick)
+            .padding(start = 8.dp),
+        contentAlignment = Alignment.CenterStart
+    ) {
+        Text(
+            text = title,
+            modifier = Modifier,
+            fontSize = 16.sp
+        )
+    }
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/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
new file mode 100644
index 0000000..5611bc4
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/integration-tests/demos/src/main/java/androidx/constraintlayout/compose/demos/ChainsDemo.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.constraintlayout.compose.demos
+
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.Button
+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.graphics.Color
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.ConstraintLayout
+import androidx.constraintlayout.compose.ConstraintSet
+import androidx.constraintlayout.compose.Dimension
+
+@Preview
+@Composable
+fun ChainsAnimatedOrientationDemo() {
+    val boxColors = listOf(Color.Red, Color.Blue, Color.Green)
+    var isHorizontal by remember { mutableStateOf(true) }
+
+    Column(Modifier.fillMaxSize()) {
+        ConstraintLayout(
+            constraintSet = ConstraintSet {
+                val (box0, box1, box2) = createRefsFor("box0", "box1", "box2")
+                box1.withChainParams(8.dp, 8.dp, 8.dp, 8.dp)
+
+                if (isHorizontal) {
+                    constrain(box0, box1, box2) {
+                        width = Dimension.fillToConstraints
+                        height = Dimension.value(20.dp)
+                        centerVerticallyTo(parent)
+                    }
+                    constrain(box1) {
+                        // Override height to be a ratio
+                        height = Dimension.ratio("2:1")
+                    }
+
+                    createHorizontalChain(box0, box1, box2)
+                } else {
+                    constrain(box0, box1, box2) {
+                        width = Dimension.value(20.dp)
+                        height = Dimension.fillToConstraints
+                        centerHorizontallyTo(parent)
+                    }
+                    constrain(box1) {
+                        // Override width to be a ratio
+                        width = Dimension.ratio("2:1")
+                    }
+
+                    createVerticalChain(box0, box1, box2)
+                }
+            },
+            animateChanges = true,
+            animationSpec = tween(800),
+            modifier = Modifier
+                .fillMaxWidth()
+                .weight(1.0f, true)
+        ) {
+            boxColors.forEachIndexed { index, color ->
+                Box(
+                    modifier = Modifier
+                        .layoutId("box$index")
+                        .background(color)
+                )
+            }
+        }
+        Button(onClick = { isHorizontal = !isHorizontal }) {
+            Text(text = "Toggle Orientation")
+        }
+    }
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/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
new file mode 100644
index 0000000..d660ec7
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/integration-tests/demos/src/main/java/androidx/constraintlayout/compose/demos/CustomKeyAttributesDemo.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalMotionApi::class)
+
+package androidx.constraintlayout.compose.demos
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.Dimension
+import androidx.constraintlayout.compose.ExperimentalMotionApi
+import androidx.constraintlayout.compose.MotionLayout
+import androidx.constraintlayout.compose.MotionScene
+import androidx.constraintlayout.compose.OnSwipe
+import androidx.constraintlayout.compose.SwipeDirection
+import androidx.constraintlayout.compose.SwipeSide
+
+@Preview
+@Composable
+fun CustomColorInKeyAttributesDemo() {
+    val boxId = "box"
+    val textId = "text"
+    MotionLayout(
+        motionScene = MotionScene {
+            val box = createRefFor(boxId)
+            val text = createRefFor(textId)
+            defaultTransition(
+                from = constraintSet {
+                    constrain(text) {
+                        top.linkTo(box.bottom, 10.dp)
+                        start.linkTo(box.start)
+                    }
+                    constrain(box) {
+                        width = Dimension.value(50.dp)
+                        height = Dimension.value(50.dp)
+
+                        centerVerticallyTo(parent)
+                        start.linkTo(parent.start)
+
+                        customColor("background", Color.Red)
+                    }
+                },
+                to = constraintSet {
+                    constrain(text) {
+                        top.linkTo(box.bottom, 10.dp)
+                        end.linkTo(box.end)
+                    }
+                    constrain(box) {
+                        width = Dimension.value(50.dp)
+                        height = Dimension.value(50.dp)
+
+                        centerVerticallyTo(parent)
+                        end.linkTo(parent.end)
+
+                        customColor("background", Color.Blue)
+                    }
+                }
+            ) {
+                onSwipe = OnSwipe(
+                    anchor = box,
+                    side = SwipeSide.Middle,
+                    direction = SwipeDirection.End
+                )
+                keyAttributes(box) {
+                    frame(33) {
+                        customColor("background", Color.Yellow)
+                    }
+                    frame(66) {
+                        customColor("background", Color.Green)
+                    }
+                }
+            }
+        },
+        progress = 0f,
+        modifier = Modifier.fillMaxSize()
+    ) {
+        val background = customColor(boxId, "background")
+        Box(
+            modifier = Modifier
+                .layoutId(boxId)
+                .background(background)
+        )
+        Text(
+            modifier = Modifier.layoutId(textId),
+            text = "Color: ${background.toArgb().toUInt().toString(16)}"
+        )
+    }
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/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
new file mode 100644
index 0000000..6a9c88f
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/integration-tests/demos/src/main/java/androidx/constraintlayout/compose/demos/OnSwipeDemos.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalMotionApi::class)
+
+package androidx.constraintlayout.compose.demos
+
+import androidx.compose.animation.core.tween
+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.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.Button
+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.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.ExperimentalMotionApi
+import androidx.constraintlayout.compose.MotionLayout
+import androidx.constraintlayout.compose.MotionLayoutDebugFlags
+import androidx.constraintlayout.compose.MotionScene
+import androidx.constraintlayout.compose.layoutId
+import androidx.constraintlayout.compose.rememberMotionLayoutState
+
+@Preview
+@Composable
+fun SimpleOnSwipe() {
+    var mode by remember {
+        mutableStateOf("spring")
+    }
+    var toEnd by remember { mutableStateOf(true) }
+    val motionLayoutState = rememberMotionLayoutState(key = mode)
+
+    val motionSceneContent = remember(mode) {
+        // language=json5
+        """
+       {
+         Header: { exportAs: 'swipeExpr' },
+         ConstraintSets: {
+           start: {
+             box: {
+               width: 50, height: 50,
+               bottom: ['parent', 'bottom', 10],
+               start: ['parent', 'start', 10],
+               custom: {
+                 bColor: '#ff0000'
+               }
+             }
+           },
+           end: {
+             Extends: 'start',
+             box: {
+               clear: ['constraints'],
+                  width: 100, height: 400,
+               top: ['parent', 'top', 10],
+               end: ['parent', 'end', 10],
+               custom: {
+                 bColor: '#0000ff'
+               }
+             }
+           }
+         },
+         Transitions: {
+           default: {
+              from: 'start',
+              to: 'end',
+              KeyFrames: {
+                KeyPositions: [{
+                  target: ['box'],
+                  frames: [25, 50, 75],
+                  percentX: [0.25, 0.5, 0.75],
+                  percentY: [0.25, 0.5, 0.75]
+                }],
+              },
+              onSwipe: {
+                anchor: 'box',
+                direction: 'end',
+                side: 'start',
+                mode: '$mode',
+                springMass: 1,
+                springDamping: 10,
+                springStiffness: 70,
+              }
+           }
+         }
+       }
+        """.trimIndent()
+    }
+    Column {
+        Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+            Button(onClick = { motionLayoutState.snapTo(0f) }) {
+                Text(text = "Reset")
+            }
+            Button(onClick = {
+                val target = if (toEnd) 1f else 0f
+                motionLayoutState.animateTo(target, tween(2000))
+                toEnd = !toEnd
+            }) {
+                Text(text = if (toEnd) "End" else "Start")
+            }
+            Button(onClick = {
+                if (motionLayoutState.isInDebugMode) {
+                    motionLayoutState.setDebugMode(MotionLayoutDebugFlags.NONE)
+                } else {
+                    motionLayoutState.setDebugMode(MotionLayoutDebugFlags.SHOW_ALL)
+                }
+            }) {
+                Text("Debug")
+            }
+            Button(onClick = {
+                mode = when (mode) {
+                    "spring" -> "velocity"
+                    else -> "spring"
+                }
+            }) {
+                Text(text = "Mode: $mode")
+            }
+        }
+        MotionLayout(
+            modifier = Modifier
+                .fillMaxWidth()
+                .weight(1.0f, fill = true),
+            motionLayoutState = motionLayoutState,
+            motionScene = MotionScene(content = motionSceneContent)
+        ) {
+            Box(
+                modifier = Modifier
+                    .background(customProperties(id = "box").color("bColor"))
+                    .layoutId("box")
+            )
+        }
+        Text(text = "Current progress: ${motionLayoutState.currentProgress}")
+    }
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/target/newmessage/NewMessage.kt b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/target/newmessage/NewMessage.kt
index 8f99fe2..8d144a9 100644
--- a/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/target/newmessage/NewMessage.kt
+++ b/constraintlayout/constraintlayout-compose/integration-tests/macrobenchmark-target/src/main/java/androidx/constraintlayout/compose/integration/macrobenchmark/target/newmessage/NewMessage.kt
@@ -485,7 +485,7 @@
     }
     Surface(
         modifier = Modifier.layoutId("box"),
-        color = motionColor(id = "box", name = "background"),
+        color = customColor(id = "box", name = "background"),
         elevation = 4.dp,
         shape = RoundedCornerShape(8.dp)
     ) {}
@@ -495,7 +495,7 @@
             NewMessageLayout.Fab -> Icons.Default.Edit
             else -> Icons.Default.Close
         },
-        color = motionColor("editClose", "content"),
+        color = customColor("editClose", "content"),
         enabled = true
     ) {
         when (currentState) {
@@ -506,7 +506,7 @@
     ColorableIconButton(
         modifier = Modifier.layoutId("minIcon"),
         imageVector = Icons.Default.KeyboardArrowDown,
-        color = motionColor("minIcon", "content"),
+        color = customColor("minIcon", "content"),
         enabled = true
     ) {
         when (currentState) {
@@ -517,7 +517,7 @@
     CheapText(
         text = dialogName,
         modifier = Modifier.layoutId("title"),
-        color = motionColor("title", "content"),
+        color = customColor("title", "content"),
         style = MaterialTheme.typography.h6
     )
     MessageWidget(modifier = Modifier.layoutId("content"), onDelete = {
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
index 1153106..4f49804 100644
--- a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -26,6 +27,7 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -34,7 +36,9 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.FirstBaseline
 import androidx.compose.ui.layout.boundsInParent
+import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.positionInParent
@@ -46,6 +50,7 @@
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.IntOffset
@@ -1928,6 +1933,70 @@
     }
 
     @Test
+    fun testConstraintSet_multipleRefs() = with(rule.density) {
+        val rootSize = 50
+        val boxSize = 20
+        val margin = 10
+        val positions = Array(3) { IntOffset.Zero }
+        rule.setContent {
+            ConstraintLayout(
+                constraintSet = ConstraintSet {
+                    // Note that not enough IDs were provided, box2 will have a generated ID
+                    val (box0, box1, box2) = createRefsFor("box0", "box1")
+
+                    constrain(box0, box1) {
+                        width = boxSize.toDp().asDimension
+                        height = boxSize.toDp().asDimension
+                        top.linkTo(parent.top, margin.toDp())
+                        start.linkTo(parent.start, margin.toDp())
+                    }
+                    constrain(box2) {
+                        width = boxSize.toDp().asDimension
+                        height = boxSize.toDp().asDimension
+
+                        top.linkTo(box0.bottom)
+                        start.linkTo(box0.end)
+                    }
+                },
+                Modifier.size(rootSize.toDp())
+            ) {
+                Box(
+                    Modifier
+                        .layoutId("box0")
+                        .onGloballyPositioned {
+                            positions[0] = it
+                                .positionInRoot()
+                                .round()
+                        }
+                )
+                Box(
+                    Modifier
+                        .layoutId("box1")
+                        .onGloballyPositioned {
+                            positions[1] = it
+                                .positionInRoot()
+                                .round()
+                        }
+                )
+                Box(
+                    Modifier
+                        // Generated id, tho normally, the user wouldn't know what the ID is
+                        .layoutId("androidx.constraintlayout.id0")
+                        .onGloballyPositioned {
+                            positions[2] = it
+                                .positionInRoot()
+                                .round()
+                        }
+                )
+            }
+        }
+        rule.waitForIdle()
+        assertEquals(IntOffset(margin, margin), positions[0])
+        assertEquals(IntOffset(margin, margin), positions[1])
+        assertEquals(IntOffset(margin + boxSize, margin + boxSize), positions[2])
+    }
+
+    @Test
     fun testLinkToBias_withInlineDsl_rtl() = with(rule.density) {
         val rootSize = 200
         val boxSize = 20
@@ -1997,6 +2066,309 @@
         }
     }
 
+    @Test
+    fun testContentRecomposition_withConstraintSet() = with(rule.density) {
+        var constraintLayoutCompCount = 0
+
+        val baseWidth = 10
+        val box0WidthMultiplier = mutableStateOf(2)
+        val boxHeight = 30
+        rule.setContent {
+            ++constraintLayoutCompCount
+            ConstraintLayout(
+                constraintSet = ConstraintSet {
+                    val (box0, box1) = createRefsFor("box0", "box1")
+                    constrain(box0) {
+                        // previously, preferredWrapContent would fail if only the content
+                        // recomposed
+                        width = Dimension.preferredWrapContent
+
+                        start.linkTo(parent.start)
+                        end.linkTo(parent.end)
+                        horizontalBias = 0f
+
+                        top.linkTo(parent.top)
+                    }
+                    constrain(box1) {
+                        width = Dimension.fillToConstraints
+                        height = Dimension.wrapContent
+                        start.linkTo(box0.start)
+                        end.linkTo(box0.end)
+                        horizontalBias = 0f
+
+                        top.linkTo(box0.bottom)
+                    }
+                }
+            ) {
+                Box(
+                    Modifier
+                        .height(boxHeight.toDp())
+                        .width((baseWidth * box0WidthMultiplier.value).toDp())
+                        .layoutTestId("box0")
+                        .background(Color.Red)
+                )
+                Box(
+                    Modifier
+                        .height(boxHeight.toDp())
+                        .layoutTestId("box1")
+                        .background(Color.Blue)
+                )
+            }
+        }
+        rule.waitForIdle()
+
+        rule.onNodeWithTag("box0").apply {
+            assertPositionInRootIsEqualTo(0.dp, 0.dp)
+            assertWidthIsEqualTo(20.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
+        }
+        rule.onNodeWithTag("box1").apply {
+            assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp())
+            assertWidthIsEqualTo(20.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
+        }
+
+        box0WidthMultiplier.value = 3
+        rule.waitForIdle()
+
+        rule.onNodeWithTag("box0").apply {
+            assertPositionInRootIsEqualTo(0.dp, 0.dp)
+            assertWidthIsEqualTo(30.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
+        }
+        rule.onNodeWithTag("box1").apply {
+            assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp())
+            assertWidthIsEqualTo(30.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
+        }
+
+        box0WidthMultiplier.value = 1
+        rule.waitForIdle()
+
+        rule.onNodeWithTag("box0").apply {
+            assertPositionInRootIsEqualTo(0.dp, 0.dp)
+            assertWidthIsEqualTo(10.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
+        }
+        rule.onNodeWithTag("box1").apply {
+            assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp())
+            assertWidthIsEqualTo(10.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
+        }
+
+        assertEquals(1, constraintLayoutCompCount)
+    }
+
+    @Test
+    fun testContentRecomposition_withInlineModifier() = with(rule.density) {
+        var constraintLayoutCompCount = 0
+
+        val baseWidth = 10
+        val box0WidthMultiplier = mutableStateOf(2)
+        val boxHeight = 30
+        rule.setContent {
+            ++constraintLayoutCompCount
+            ConstraintLayout {
+                val (box0, box1) = createRefs()
+                Box(
+                    Modifier
+                        .height(boxHeight.toDp())
+                        .width((baseWidth * box0WidthMultiplier.value).toDp())
+                        .constrainAs(box0) {
+                            // previously, preferredWrapContent would fail if only the content
+                            // recomposed
+                            width = Dimension.preferredWrapContent
+
+                            start.linkTo(parent.start)
+                            end.linkTo(parent.end)
+                            horizontalBias = 0f
+
+                            top.linkTo(parent.top)
+                        }
+                        .testTag("box0")
+                        .background(Color.Red)
+                )
+                Box(
+                    Modifier
+                        .height(boxHeight.toDp())
+                        .constrainAs(box1) {
+                            width = Dimension.fillToConstraints
+                            height = Dimension.wrapContent
+                            start.linkTo(box0.start)
+                            end.linkTo(box0.end)
+                            horizontalBias = 0f
+
+                            top.linkTo(box0.bottom)
+                        }
+                        .testTag("box1")
+                        .background(Color.Blue)
+                )
+            }
+        }
+        rule.waitForIdle()
+
+        rule.onNodeWithTag("box0").apply {
+            assertPositionInRootIsEqualTo(0.dp, 0.dp)
+            assertWidthIsEqualTo(20.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
+        }
+        rule.onNodeWithTag("box1").apply {
+            assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp())
+            assertWidthIsEqualTo(20.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
+        }
+
+        box0WidthMultiplier.value = 3
+        rule.waitForIdle()
+
+        rule.onNodeWithTag("box0").apply {
+            assertPositionInRootIsEqualTo(0.dp, 0.dp)
+            assertWidthIsEqualTo(30.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
+        }
+        rule.onNodeWithTag("box1").apply {
+            assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp())
+            assertWidthIsEqualTo(30.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
+        }
+
+        box0WidthMultiplier.value = 1
+        rule.waitForIdle()
+
+        rule.onNodeWithTag("box0").apply {
+            assertPositionInRootIsEqualTo(0.dp, 0.dp)
+            assertWidthIsEqualTo(10.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
+        }
+        rule.onNodeWithTag("box1").apply {
+            assertPositionInRootIsEqualTo(0.dp, boxHeight.toDp())
+            assertWidthIsEqualTo(10.toDp()) // (box0WidthMultiplier.value * baseWidth).toDp()
+        }
+
+        assertEquals(1, constraintLayoutCompCount)
+    }
+
+    @Test
+    fun testBaselineConstraints() = with(rule.density) {
+        fun Modifier.withBaseline() = this.layout { measurable, constraints ->
+            val placeable = measurable.measure(constraints)
+            val halfHeight = (placeable.height / 2f).roundToInt()
+            layout(
+                width = placeable.width,
+                height = placeable.height,
+                alignmentLines = mapOf(FirstBaseline to halfHeight)
+            ) {
+                placeable.place(0, 0)
+            }
+        }
+
+        val boxSize = 10
+        val box1Margin = 13
+        val box2Margin = -7f
+
+        var box1Position = IntOffset.Zero
+        var box2Position = IntOffset.Zero
+        rule.setContent {
+            ConstraintLayout {
+                val (box1, box2) = createRefs()
+                Box(
+                    Modifier
+                        .size(boxSize.toDp())
+                        .withBaseline()
+                        .constrainAs(box1) {
+                            baseline.linkTo(parent.top, box1Margin.toDp())
+                            start.linkTo(parent.start)
+                        }
+                        .onGloballyPositioned {
+                            box1Position = it
+                                .positionInRoot()
+                                .round()
+                        })
+                Box(
+                    Modifier
+                        .size(boxSize.toDp())
+                        .withBaseline()
+                        .constrainAs(box2) {
+                            top.linkTo(box1.baseline, box2Margin.toDp())
+                        }
+                        .onGloballyPositioned {
+                            box2Position = it
+                                .positionInRoot()
+                                .round()
+                        })
+            }
+        }
+        val expectedBox1Y = box1Margin - (boxSize * 0.5f).roundToInt()
+        val expectedBox2Y = expectedBox1Y + box2Margin + (boxSize * 0.5f).toInt()
+        rule.runOnIdle {
+            assertEquals(IntOffset(0, expectedBox1Y), box1Position)
+            assertEquals(IntOffset(0, expectedBox2Y.roundToInt()), box2Position)
+        }
+    }
+
+    @Test
+    fun testConstraintLayout_withParentIntrinsics() = with(rule.density) {
+        val rootBoxWidth = 200
+        val box1Size = 40
+        val box2Size = 70
+
+        var rootSize = IntSize.Zero
+        var clSize = IntSize.Zero
+        var box1Position = IntOffset.Zero
+        var box2Position = IntOffset.Zero
+
+        rule.setContent {
+            Box(
+                modifier = Modifier
+                    .width(rootBoxWidth.toDp())
+                    .height(IntrinsicSize.Max)
+                    .background(Color.LightGray)
+                    .onGloballyPositioned {
+                        rootSize = it.size
+                    }
+            ) {
+                ConstraintLayout(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .wrapContentHeight()
+                        .background(Color.Yellow)
+                        .onGloballyPositioned {
+                            clSize = it.size
+                        }
+                ) {
+                    val (one, two) = createRefs()
+                    val horChain =
+                        createHorizontalChain(one, two, chainStyle = ChainStyle.Packed(0f))
+                    constrain(horChain) {
+                        start.linkTo(parent.start)
+                        end.linkTo(parent.end)
+                    }
+                    Box(
+                        Modifier
+                            .size(box1Size.toDp())
+                            .background(Color.Green)
+                            .constrainAs(one) {
+                                top.linkTo(parent.top)
+                                bottom.linkTo(parent.bottom)
+                            }
+                            .onGloballyPositioned {
+                                box1Position = it.positionInRoot().round()
+                            })
+                    Box(
+                        Modifier
+                            .size(box2Size.toDp())
+                            .background(Color.Red)
+                            .constrainAs(two) {
+                                width = Dimension.preferredWrapContent
+                                top.linkTo(parent.top)
+                                bottom.linkTo(parent.bottom)
+                            }
+                            .onGloballyPositioned {
+                                box2Position = it.positionInRoot().round()
+                            })
+                }
+            }
+        }
+
+        val expectedSize = IntSize(rootBoxWidth, box2Size)
+        val expectedBox1Y = ((box2Size / 2f) - (box1Size / 2f)).roundToInt()
+        rule.runOnIdle {
+            assertEquals(expectedSize, rootSize)
+            assertEquals(expectedSize, clSize)
+            assertEquals(IntOffset(0, expectedBox1Y), box1Position)
+            assertEquals(IntOffset(box1Size, 0), box2Position)
+        }
+    }
+
     private fun listAnchors(box: ConstrainedLayoutReference): List<ConstrainScope.() -> Unit> {
         // TODO(172055763) directly construct an immutable list when Lint supports it
         val anchors = mutableListOf<ConstrainScope.() -> Unit>()
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/MotionLayoutTest.kt b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt
index 980a920..1709620 100644
--- a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt
@@ -22,7 +22,12 @@
 import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+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.layout.wrapContentHeight
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.material.LocalTextStyle
 import androidx.compose.material.Text
@@ -38,6 +43,8 @@
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.test.getUnclippedBoundsInRoot
@@ -48,12 +55,16 @@
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.round
 import androidx.compose.ui.unit.size
 import androidx.compose.ui.unit.sp
 import androidx.constraintlayout.compose.test.R
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import kotlin.math.roundToInt
 import kotlin.test.assertEquals
 import org.junit.Rule
 import org.junit.Test
@@ -67,7 +78,7 @@
     val rule = createComposeRule()
 
     /**
-     * Tests that [MotionLayoutScope.motionFontSize] works as expected.
+     * Tests that [MotionLayoutScope.customFontSize] works as expected.
      *
      * See custom_text_size_scene.json5
      */
@@ -138,19 +149,33 @@
                 progress = progress.value,
                 modifier = Modifier.size(200.dp)
             ) {
-                val props by motionProperties(id = "element")
+                val props = customProperties(id = "element")
                 Column(Modifier.layoutId("element")) {
                     Text(
-                        text = "Color: #${props.color("color").toArgb().toUInt().toString(16)}"
+                        text = "1) Color: #${props.color("color").toHexString()}"
                     )
                     Text(
-                        text = "Distance: ${props.distance("distance")}"
+                        text = "2) Distance: ${props.distance("distance")}"
                     )
                     Text(
-                        text = "FontSize: ${props.fontSize("fontSize")}"
+                        text = "3) FontSize: ${props.fontSize("fontSize")}"
                     )
                     Text(
-                        text = "Int: ${props.int("int")}"
+                        text = "4) Int: ${props.int("int")}"
+                    )
+
+                    // Missing properties
+                    Text(
+                        text = "5) Color: #${props.color("a").toHexString()}"
+                    )
+                    Text(
+                        text = "6) Distance: ${props.distance("b")}"
+                    )
+                    Text(
+                        text = "7) FontSize: ${props.fontSize("c")}"
+                    )
+                    Text(
+                        text = "8) Int: ${props.int("d")}"
                     )
                 }
             }
@@ -159,18 +184,114 @@
 
         progress.value = 0.25f
         rule.waitForIdle()
-        rule.onNodeWithText("Color: #ffffbaba").assertExists()
-        rule.onNodeWithText("Distance: 10.0.dp").assertExists()
-        rule.onNodeWithText("FontSize: 15.0.sp").assertExists()
-        rule.onNodeWithText("Int: 20").assertExists()
+        rule.onNodeWithText("1) Color: #ffffbaba").assertExists()
+        rule.onNodeWithText("2) Distance: 10.0.dp").assertExists()
+        rule.onNodeWithText("3) FontSize: 15.0.sp").assertExists()
+        rule.onNodeWithText("4) Int: 20").assertExists()
+
+        // Undefined custom properties
+        rule.onNodeWithText("5) Color: #0").assertExists()
+        rule.onNodeWithText("6) Distance: Dp.Unspecified").assertExists()
+        rule.onNodeWithText("7) FontSize: NaN.sp").assertExists()
+        rule.onNodeWithText("8) Int: 0").assertExists()
 
         progress.value = 0.75f
         rule.waitForIdle()
-        rule.onNodeWithText("Color: #ffba0000").assertExists()
-        rule.onNodeWithText("Distance: 15.0.dp").assertExists()
-        rule.onNodeWithText("FontSize: 25.0.sp").assertExists()
-        rule.onNodeWithText("Int: 35").assertExists()
+        rule.onNodeWithText("1) Color: #ffba0000").assertExists()
+        rule.onNodeWithText("2) Distance: 15.0.dp").assertExists()
+        rule.onNodeWithText("3) FontSize: 25.0.sp").assertExists()
+        rule.onNodeWithText("4) Int: 35").assertExists()
+
+        // Undefined custom properties
+        rule.onNodeWithText("5) Color: #0").assertExists()
+        rule.onNodeWithText("6) Distance: Dp.Unspecified").assertExists()
+        rule.onNodeWithText("7) FontSize: NaN.sp").assertExists()
+        rule.onNodeWithText("8) Int: 0").assertExists()
     }
+
+    @Test
+    fun testMotionLayout_withParentIntrinsics() = with(rule.density) {
+        val constraintSet = ConstraintSet {
+            val (one, two) = createRefsFor("one", "two")
+            val horChain = createHorizontalChain(one, two, chainStyle = ChainStyle.Packed(0f))
+            constrain(horChain) {
+                start.linkTo(parent.start)
+                end.linkTo(parent.end)
+            }
+            constrain(one) {
+                top.linkTo(parent.top)
+                bottom.linkTo(parent.bottom)
+            }
+            constrain(two) {
+                width = Dimension.preferredWrapContent
+                top.linkTo(parent.top)
+                bottom.linkTo(parent.bottom)
+            }
+        }
+
+        val rootBoxWidth = 200
+        val box1Size = 40
+        val box2Size = 70
+
+        var rootSize = IntSize.Zero
+        var mlSize = IntSize.Zero
+        var box1Position = IntOffset.Zero
+        var box2Position = IntOffset.Zero
+
+        rule.setContent {
+            Box(
+                modifier = Modifier
+                    .width(rootBoxWidth.toDp())
+                    .height(IntrinsicSize.Max)
+                    .background(Color.LightGray)
+                    .onGloballyPositioned {
+                        rootSize = it.size
+                    }
+            ) {
+                MotionLayout(
+                    start = constraintSet,
+                    end = constraintSet,
+                    transition = Transition {},
+                    progress = 0f, // We're not testing the animation
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .wrapContentHeight()
+                        .background(Color.Yellow)
+                        .onGloballyPositioned {
+                            mlSize = it.size
+                        }
+                ) {
+                    Box(
+                        Modifier
+                            .size(box1Size.toDp())
+                            .background(Color.Green)
+                            .layoutId("one")
+                            .onGloballyPositioned {
+                                box1Position = it.positionInRoot().round()
+                            })
+                    Box(
+                        Modifier
+                            .size(box2Size.toDp())
+                            .background(Color.Red)
+                            .layoutId("two")
+                            .onGloballyPositioned {
+                                box2Position = it.positionInRoot().round()
+                            })
+                }
+            }
+        }
+
+        val expectedSize = IntSize(rootBoxWidth, box2Size)
+        val expectedBox1Y = ((box2Size / 2f) - (box1Size / 2f)).roundToInt()
+        rule.runOnIdle {
+            assertEquals(expectedSize, rootSize)
+            assertEquals(expectedSize, mlSize)
+            assertEquals(IntOffset(0, expectedBox1Y), box1Position)
+            assertEquals(IntOffset(box1Size, 0), box2Position)
+        }
+    }
+
+    private fun Color.toHexString(): String = toArgb().toUInt().toString(16)
 }
 
 @OptIn(ExperimentalMotionApi::class)
@@ -195,7 +316,7 @@
             progress = progress,
             modifier = modifier
         ) {
-            val profilePicProperties = motionProperties(id = "profile_pic")
+            val profilePicProperties = customProperties(id = "profile_pic")
             Box(
                 modifier = Modifier
                     .layoutTestId("box")
@@ -208,16 +329,16 @@
                     .clip(CircleShape)
                     .border(
                         width = 2.dp,
-                        color = profilePicProperties.value.color("background"),
+                        color = profilePicProperties.color("background"),
                         shape = CircleShape
                     )
                     .layoutTestId("profile_pic")
             )
             Text(
                 text = "Hello",
-                fontSize = motionFontSize("username", "textSize"),
+                fontSize = customFontSize("username", "textSize"),
                 modifier = Modifier.layoutTestId("username"),
-                color = profilePicProperties.value.color("background")
+                color = profilePicProperties.color("background")
             )
         }
     }
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MultiMeasureCompositionTest.kt b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MultiMeasureCompositionTest.kt
new file mode 100644
index 0000000..5f80af3
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MultiMeasureCompositionTest.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.constraintlayout.compose
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.neverEqualPolicy
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.MultiMeasureLayout
+import androidx.compose.ui.node.Ref
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+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 kotlin.test.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val HEIGHT_FROM_CONTENT = 40
+private const val HEIGHT_FROM_CALLER = 80
+
+/**
+ * This class tests a couple of assumptions that ConstraintLayout & MotionLayout need to operate
+ * properly.
+ *
+ * See [MaxWrapContentWithMultiMeasure].
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class MultiMeasureCompositionTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun testCustomMultiMeasure_changesFromCompositionSource(): Unit = with(rule.density) {
+        var callerCompositionCount = 0
+        var contentCompositionCount = 0
+
+        val minWidth = 40
+
+        // Mutable state that is only read in the content, will not directly recompose our
+        // MultiMeasure Composable
+        val widthMultiplier = mutableStateOf(4)
+        val baseWidth = 10
+
+        // Mutable state that is read at the same scope of our MultiMeasure Composable, will cause
+        // it to recompose, but does not directly affect the content
+        val unusedValue = mutableStateOf(0)
+        rule.setContent {
+            Column(
+                Modifier
+                    .fillMaxSize()
+                    .background(Color.LightGray)
+            ) {
+                ++callerCompositionCount
+                unusedValue.value
+                MaxWrapContentWithMultiMeasure {
+                    ++contentCompositionCount
+                    // Box with variable width, depends on the multiplier value
+                    Box(
+                        modifier = Modifier
+                            .width((widthMultiplier.value * baseWidth).toDp())
+                            .background(Color.Red)
+                            .testTag("box0")
+                    )
+                    // Box with constant width
+                    Box(
+                        Modifier
+                            .width(minWidth.toDp())
+                            .background(Color.Blue)
+                            .testTag("box1")
+                    )
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        // Assert the initial layout, composed from the root, so height is HEIGHT_FROM_CALLER
+        rule.onNodeWithTag("box0").apply {
+            assertPositionInRootIsEqualTo(0.dp, 0.dp)
+            assertWidthIsEqualTo(minWidth.toDp())
+            assertHeightIsEqualTo(HEIGHT_FROM_CALLER.toDp())
+        }
+        rule.onNodeWithTag("box1").apply {
+            assertPositionInRootIsEqualTo(0.dp, HEIGHT_FROM_CALLER.toDp())
+            assertWidthIsEqualTo(minWidth.toDp())
+            assertHeightIsEqualTo(HEIGHT_FROM_CALLER.toDp())
+        }
+
+        rule.runOnIdle {
+            // Increase multiplier, this will cause the layout to recompose from the content
+            widthMultiplier.value = widthMultiplier.value + 1
+        }
+        rule.waitForIdle()
+
+        // MaxWrapContentWithMultiMeasure assigns different height when recomposed from the content
+        rule.onNodeWithTag("box0").apply {
+            assertPositionInRootIsEqualTo(0.dp, 0.dp)
+            assertWidthIsEqualTo(50.toDp()) // baseWidth * widthMultiplier.value
+            assertHeightIsEqualTo(HEIGHT_FROM_CONTENT.toDp())
+        }
+        rule.onNodeWithTag("box1").apply {
+            assertPositionInRootIsEqualTo(0.dp, HEIGHT_FROM_CONTENT.toDp())
+            assertWidthIsEqualTo(50.toDp()) // baseWidth * widthMultiplier.value
+            assertHeightIsEqualTo(HEIGHT_FROM_CONTENT.toDp())
+        }
+
+        rule.runOnIdle {
+            // Decrease multiplier
+            widthMultiplier.value = 3
+        }
+        rule.waitForIdle()
+
+        // Verify layout is still correct
+        rule.onNodeWithTag("box0").apply {
+            assertPositionInRootIsEqualTo(0.dp, 0.dp)
+            assertWidthIsEqualTo(minWidth.toDp())
+            assertHeightIsEqualTo(HEIGHT_FROM_CONTENT.toDp())
+        }
+        rule.onNodeWithTag("box1").apply {
+            assertPositionInRootIsEqualTo(0.dp, HEIGHT_FROM_CONTENT.toDp())
+            assertWidthIsEqualTo(minWidth.toDp())
+            assertHeightIsEqualTo(HEIGHT_FROM_CONTENT.toDp())
+        }
+
+        rule.runOnIdle {
+            // This causes a recomposition from the caller of our Composable
+            unusedValue.value = 1
+        }
+        rule.waitForIdle()
+
+        // MaxWrapContentWithMultiMeasure assigns different height when recomposed from the Caller
+        rule.onNodeWithTag("box0").apply {
+            assertPositionInRootIsEqualTo(0.dp, 0.dp)
+            assertWidthIsEqualTo(minWidth.toDp())
+            assertHeightIsEqualTo(HEIGHT_FROM_CALLER.toDp())
+        }
+        rule.onNodeWithTag("box1").apply {
+            assertPositionInRootIsEqualTo(0.dp, HEIGHT_FROM_CALLER.toDp())
+            assertWidthIsEqualTo(minWidth.toDp())
+            assertHeightIsEqualTo(HEIGHT_FROM_CALLER.toDp())
+        }
+
+        rule.runOnIdle {
+            assertEquals(2, callerCompositionCount)
+            assertEquals(4, contentCompositionCount)
+        }
+    }
+
+    /**
+     * Column-like layout that assigns the max WrapContent width to all its children.
+     *
+     * Note that the height assigned height will depend on where the recomposition started: 40px if it
+     * started from the content, 80px if it started from the Composable caller.
+     */
+    @Composable
+    inline fun MaxWrapContentWithMultiMeasure(
+        modifier: Modifier = Modifier,
+        crossinline content: @Composable () -> Unit
+    ) {
+        val compTracker = remember { mutableStateOf(Unit, neverEqualPolicy()) }
+        val compSource =
+            remember { Ref<CompositionSource>().apply { value = CompositionSource.Unknown } }
+        compSource.value = CompositionSource.Caller
+
+        @Suppress("DEPRECATION")
+        MultiMeasureLayout(
+            modifier = modifier,
+            measurePolicy = maxWidthPolicy(compTracker, compSource),
+            content = {
+                // Reassign the mutable state, so that readers recompose with the content
+                compTracker.value = Unit
+
+                if (compSource.value == CompositionSource.Unknown) {
+                    compSource.value = CompositionSource.Content
+                }
+                content()
+            }
+        )
+    }
+
+    fun maxWidthPolicy(
+        compTracker: State<Unit>,
+        compSource: Ref<CompositionSource>
+    ): MeasurePolicy =
+        MeasurePolicy { measurables, constraints ->
+            // This state read will force the MeasurePolicy to re-run whenever the content
+            // recomposes, even if our Composable didn't
+            compTracker.value
+
+            val height = when (compSource.value) {
+                CompositionSource.Content -> HEIGHT_FROM_CONTENT
+                CompositionSource.Caller -> HEIGHT_FROM_CALLER
+                CompositionSource.Unknown,
+                null -> 0
+            }
+
+            compSource.value = CompositionSource.Unknown
+
+            // Find the max WrapContent width
+            val maxWrapWidth = measurables.map {
+                it.measure(constraints.copy(minWidth = 0, minHeight = height))
+            }.maxOf {
+                it.width
+            }
+
+            // Remeasure, assign the maxWrapWidth to every child
+            val placeables = measurables.map {
+                it.measure(constraints.copy(minWidth = maxWrapWidth, minHeight = height))
+            }
+
+            // Wrap the layout height to the content in a column
+            var layoutHeight = 0
+            placeables.forEach { layoutHeight += it.height }
+
+            // Position the children.
+            layout(maxWrapWidth, layoutHeight) {
+                var y = 0
+                placeables.forEach { placeable ->
+                    // Position left-aligned, one after another
+                    placeable.place(x = 0, y = y)
+                    y += placeable.height
+                }
+            }
+        }
+
+    enum class CompositionSource {
+        Unknown,
+        Caller,
+        Content
+    }
+}
\ 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/ConstrainScope.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstrainScope.kt
index eed1540..cd5d8c491 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstrainScope.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstrainScope.kt
@@ -449,6 +449,14 @@
         containerObject.remove("pivotY")
     }
 
+    /**
+     * Convenience extension variable to parse a [Dp] as a [Dimension] object.
+     *
+     * @see Dimension.value
+     */
+    val Dp.asDimension: Dimension
+        get() = Dimension.value(this)
+
     private inner class DimensionProperty(initialValue: Dimension) :
         ObservableProperty<Dimension>(initialValue) {
         override fun afterChange(property: KProperty<*>, oldValue: Dimension, newValue: Dimension) {
@@ -520,4 +528,22 @@
         }
         containerObject.put("baseline", constraintArray)
     }
+
+    /**
+     * Adds a link towards a [ConstraintLayoutBaseScope.HorizontalAnchor].
+     */
+    override fun linkTo(
+        anchor: ConstraintLayoutBaseScope.HorizontalAnchor,
+        margin: Dp,
+        goneMargin: Dp
+    ) {
+        val targetAnchorName = AnchorFunctions.horizontalAnchorIndexToAnchorName(anchor.index)
+        val constraintArray = CLArray(charArrayOf()).apply {
+            add(CLString.from(anchor.id.toString()))
+            add(CLString.from(targetAnchorName))
+            add(CLNumber(margin.value))
+            add(CLNumber(goneMargin.value))
+        }
+        containerObject.put("baseline", constraintArray)
+    }
 }
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
index b6d7025..06e001d 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintLayout.kt
@@ -41,6 +41,7 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.neverEqualPolicy
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.SnapshotStateObserver
@@ -120,17 +121,42 @@
     val measurer = remember { Measurer(density) }
     val scope = remember { ConstraintLayoutScope() }
     val remeasureRequesterState = remember { mutableStateOf(false) }
-    val (measurePolicy, onHelpersChanged) = rememberConstraintLayoutMeasurePolicy(
-        optimizationLevel,
-        scope,
-        remeasureRequesterState,
-        measurer
-    )
+    val constraintSet = remember { ConstraintSetForInlineDsl(scope) }
+    val contentTracker = remember { mutableStateOf(Unit, neverEqualPolicy()) }
+
+    val measurePolicy = MeasurePolicy { measurables, constraints ->
+        contentTracker.value
+        val layoutSize = measurer.performMeasure(
+            constraints,
+            layoutDirection,
+            constraintSet,
+            measurables,
+            optimizationLevel
+        )
+        // We read the remeasurement requester state, to request remeasure when the value
+        // changes. This will happen when the scope helpers are changing at recomposition.
+        remeasureRequesterState.value
+
+        layout(layoutSize.width, layoutSize.height) {
+            with(measurer) { performLayout(measurables) }
+        }
+    }
+
+    val onHelpersChanged = {
+        // If the helpers have changed, we need to request remeasurement. To achieve this,
+        // we are changing this boolean state that is read during measurement.
+        remeasureRequesterState.value = !remeasureRequesterState.value
+        constraintSet.knownDirty = true
+    }
+
     @Suppress("Deprecation")
     MultiMeasureLayout(
         modifier = modifier.semantics { designInfoProvider = measurer },
         measurePolicy = measurePolicy,
         content = {
+            // Perform a reassignment to the State tracker, this will force readers to recompose at
+            // the same pass as the content. The only expected reader is our MeasurePolicy.
+            contentTracker.value = Unit
             val previousHelpersHashCode = scope.helpersHashCode
             scope.reset()
             scope.content()
@@ -144,46 +170,8 @@
     )
 }
 
-@Composable
 @PublishedApi
-internal fun rememberConstraintLayoutMeasurePolicy(
-    optimizationLevel: Int,
-    scope: ConstraintLayoutScope,
-    remeasureRequesterState: MutableState<Boolean>,
-    measurer: Measurer
-): Pair<MeasurePolicy, () -> Unit> {
-    val constraintSet = remember { ConstraintSetForInlineDsl(scope) }
-
-    return remember(optimizationLevel) {
-        val measurePolicy = MeasurePolicy { measurables, constraints ->
-            val layoutSize = measurer.performMeasure(
-                constraints,
-                layoutDirection,
-                constraintSet,
-                measurables,
-                optimizationLevel
-            )
-            // We read the remeasurement requester state, to request remeasure when the value
-            // changes. This will happen when the scope helpers are changing at recomposition.
-            remeasureRequesterState.value
-
-            layout(layoutSize.width, layoutSize.height) {
-                with(measurer) { performLayout(measurables) }
-            }
-        }
-
-        val onHelpersChanged = {
-            // If the helpers have changed, we need to request remeasurement. To achieve this,
-            // we are changing this boolean state that is read during measurement.
-            remeasureRequesterState.value = !remeasureRequesterState.value
-            constraintSet.knownDirty = true
-        }
-
-        measurePolicy to onHelpersChanged
-    }
-}
-
-private class ConstraintSetForInlineDsl(
+internal class ConstraintSetForInlineDsl(
     val scope: ConstraintLayoutScope
 ) : ConstraintSet, RememberObserver {
     private var handler: Handler? = null
@@ -263,7 +251,7 @@
     animateChanges: Boolean = false,
     animationSpec: AnimationSpec<Float> = tween<Float>(),
     noinline finishedAnimationListener: (() -> Unit)? = null,
-    noinline content: @Composable () -> Unit
+    crossinline content: @Composable () -> Unit
 ) {
     if (animateChanges) {
         var startConstraint by remember { mutableStateOf(constraintSet) }
@@ -304,14 +292,26 @@
             mutableStateOf(0L)
         }
 
+        val contentTracker = remember { mutableStateOf(Unit, neverEqualPolicy()) }
         val density = LocalDensity.current
         val measurer = remember { Measurer(density) }
-        val measurePolicy = rememberConstraintLayoutMeasurePolicy(
-            optimizationLevel,
-            needsUpdate,
-            constraintSet,
-            measurer
-        )
+        remember(constraintSet) {
+            measurer.parseDesignElements(constraintSet)
+            true
+        }
+        val measurePolicy = MeasurePolicy { measurables, constraints ->
+            contentTracker.value
+            val layoutSize = measurer.performMeasure(
+                constraints,
+                layoutDirection,
+                constraintSet,
+                measurables,
+                optimizationLevel
+            )
+            layout(layoutSize.width, layoutSize.height) {
+                with(measurer) { performLayout(measurables) }
+            }
+        }
         if (constraintSet is EditableJSONLayout) {
             constraintSet.setUpdateFlag(needsUpdate)
         }
@@ -340,6 +340,10 @@
                 modifier = modifier.semantics { designInfoProvider = measurer },
                 measurePolicy = measurePolicy,
                 content = {
+                    // Perform a reassignment to the State tracker, this will force readers to
+                    // recompose at the same pass as the content. The only expected reader is our
+                    // MeasurePolicy.
+                    contentTracker.value = Unit
                     measurer.createDesignElements()
                     content()
                 }
@@ -348,29 +352,6 @@
     }
 }
 
-@Composable
-@PublishedApi
-internal fun rememberConstraintLayoutMeasurePolicy(
-    optimizationLevel: Int,
-    needsUpdate: MutableState<Long>,
-    constraintSet: ConstraintSet,
-    measurer: Measurer
-) = remember(optimizationLevel, needsUpdate.value, constraintSet) {
-    measurer.parseDesignElements(constraintSet)
-    MeasurePolicy { measurables, constraints ->
-        val layoutSize = measurer.performMeasure(
-            constraints,
-            layoutDirection,
-            constraintSet,
-            measurables,
-            optimizationLevel
-        )
-        layout(layoutSize.width, layoutSize.height) {
-            with(measurer) { performLayout(measurables) }
-        }
-    }
-}
-
 /**
  * Scope used by the inline DSL of [ConstraintLayout].
  */
@@ -462,11 +443,90 @@
 @LayoutScopeMarker
 class ConstraintSetScope internal constructor(extendFrom: CLObject?) :
     ConstraintLayoutBaseScope(extendFrom) {
+    private var generatedCount = 0
+
+    /**
+     * Generate an ID to be used as fallback if the user didn't provide enough parameters to
+     * [createRefsFor].
+     *
+     * Not intended to be used, but helps prevent runtime issues.
+     */
+    private fun nextId() = "androidx.constraintlayout.id" + generatedCount++
+
     /**
      * Creates one [ConstrainedLayoutReference] corresponding to the [ConstraintLayout] element
      * with [id].
      */
     fun createRefFor(id: Any): ConstrainedLayoutReference = ConstrainedLayoutReference(id)
+
+    /**
+     * Convenient way to create multiple [ConstrainedLayoutReference] with one statement, the [ids]
+     * provided should match Composables within ConstraintLayout using [Modifier.layoutId].
+     *
+     * Example:
+     * ```
+     * val (box, text, button) = createRefsFor("box", "text", "button")
+     * ```
+     * Note that the number of ids should match the number of variables assigned.
+     *
+     * &nbsp;
+     *
+     * To create a singular [ConstrainedLayoutReference] see [createRefFor].
+     */
+    fun createRefsFor(vararg ids: Any): ConstrainedLayoutReferences =
+        ConstrainedLayoutReferences(arrayOf(*ids))
+
+    inner class ConstrainedLayoutReferences internal constructor(
+        private val ids: Array<Any>
+    ) {
+        operator fun component1(): ConstrainedLayoutReference =
+            ConstrainedLayoutReference(ids.getOrElse(0) { nextId() })
+
+        operator fun component2(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(1) { nextId() })
+
+        operator fun component3(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(2) { nextId() })
+
+        operator fun component4(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(3) { nextId() })
+
+        operator fun component5(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(4) { nextId() })
+
+        operator fun component6(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(5) { nextId() })
+
+        operator fun component7(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(6) { nextId() })
+
+        operator fun component8(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(7) { nextId() })
+
+        operator fun component9(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(8) { nextId() })
+
+        operator fun component10(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(9) { nextId() })
+
+        operator fun component11(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(10) { nextId() })
+
+        operator fun component12(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(11) { nextId() })
+
+        operator fun component13(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(12) { nextId() })
+
+        operator fun component14(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(13) { nextId() })
+
+        operator fun component15(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(14) { nextId() })
+
+        operator fun component16(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(15) { nextId() })
+    }
 }
 
 /**
@@ -1298,6 +1358,8 @@
                 SolverDimension.createWrap().min(constraints.minHeight)
             }
         )
+        state.mParent.width.apply(state, root, ConstraintWidget.HORIZONTAL)
+        state.mParent.height.apply(state, root, ConstraintWidget.VERTICAL)
         // Build constraint set and apply it to the state.
         state.rootIncomingConstraints = constraints
         state.isLtr = layoutDirection == LayoutDirection.Ltr
@@ -1310,6 +1372,7 @@
         } else {
             buildMapping(state, measurables)
         }
+
         applyRootSize(constraints)
         root.updateHierarchy()
 
@@ -1330,24 +1393,6 @@
         root.optimizationLevel = optimizationLevel
         root.measure(root.optimizationLevel, 0, 0, 0, 0, 0, 0, 0, 0)
 
-        for (child in root.children) {
-            val measurable = child.companionWidget
-            if (measurable !is Measurable) continue
-            val placeable = placeables[measurable]
-            val currentWidth = placeable?.width
-            val currentHeight = placeable?.height
-            if (child.width != currentWidth || child.height != currentHeight) {
-                if (DEBUG) {
-                    Log.d(
-                        "CCL",
-                        "Final measurement for ${measurable.layoutId} " +
-                            "to confirm size ${child.width} ${child.height}"
-                    )
-                }
-                measurable.measure(Constraints.fixed(child.width, child.height))
-                    .also { placeables[measurable] = it }
-            }
-        }
         if (DEBUG) {
             Log.d("CCL", "ConstraintLayout is at the end ${root.width} ${root.height}")
         }
@@ -1675,6 +1720,11 @@
  * [ConstraintLayoutParentData.ref] or [ConstraintLayoutTagParentData.constraintLayoutId].
  *
  * The Tag is set from [ConstraintLayoutTagParentData.constraintLayoutTag].
+ *
+ * This should always be performed for every Measure call, since there's no guarantee that the
+ * [Measurable]s will be the same instance, even if there's seemingly no changes.
+ * Should be called before applying the [State] or, if there's no need to apply it, should be called
+ * before measuring.
  */
 internal fun buildMapping(state: State, measurables: List<Measurable>) {
     measurables.fastForEach { measurable ->
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 994d2ea..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
@@ -131,6 +131,18 @@
     ): ConstrainScope = ConstrainScope(ref.id, ref.asCLContainer()).apply(constrainBlock)
 
     /**
+     * Convenient way to apply the same constraints to multiple [ConstrainedLayoutReference]s.
+     */
+    fun constrain(
+        vararg refs: ConstrainedLayoutReference,
+        constrainBlock: ConstrainScope.() -> Unit
+    ) {
+        refs.forEach { ref ->
+            constrain(ref, constrainBlock)
+        }
+    }
+
+    /**
      * Creates a guideline at a specific offset from the start of the [ConstraintLayout].
      */
     fun createGuidelineFromStart(offset: Dp): VerticalAnchor {
@@ -679,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/ConstraintScopeCommon.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintScopeCommon.kt
index 5e8c2b3..11c8a8e 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintScopeCommon.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintScopeCommon.kt
@@ -55,6 +55,15 @@
         margin: Dp = 0.dp,
         goneMargin: Dp = 0.dp
     )
+
+    /**
+     * Adds a link towards a [ConstraintLayoutBaseScope.BaselineAnchor].
+     */
+    fun linkTo(
+        anchor: ConstraintLayoutBaseScope.BaselineAnchor,
+        margin: Dp = 0.dp,
+        goneMargin: Dp = 0.dp
+    )
 }
 
 @JvmDefaultWithCompatibility
@@ -71,6 +80,15 @@
         margin: Dp = 0.dp,
         goneMargin: Dp = 0.dp
     )
+
+    /**
+     * Adds a link towards a [ConstraintLayoutBaseScope.HorizontalAnchor].
+     */
+    fun linkTo(
+        anchor: ConstraintLayoutBaseScope.HorizontalAnchor,
+        margin: Dp = 0.dp,
+        goneMargin: Dp = 0.dp
+    )
 }
 
 internal abstract class BaseVerticalAnchorable(
@@ -115,6 +133,20 @@
         }
         containerObject.put(anchorName, constraintArray)
     }
+
+    final override fun linkTo(
+        anchor: ConstraintLayoutBaseScope.BaselineAnchor,
+        margin: Dp,
+        goneMargin: Dp
+    ) {
+        val constraintArray = CLArray(charArrayOf()).apply {
+            add(CLString.from(anchor.id.toString()))
+            add(CLString.from("baseline"))
+            add(CLNumber(margin.value))
+            add(CLNumber(goneMargin.value))
+        }
+        containerObject.put(anchorName, constraintArray)
+    }
 }
 
 internal object AnchorFunctions {
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintSet.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintSet.kt
index 5a3c088..0b97860 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintSet.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ConstraintSet.kt
@@ -51,7 +51,6 @@
     val extendFrom: ConstraintSet?
 
     override fun applyTo(state: State, measurables: List<Measurable>) {
-        buildMapping(state, measurables)
         extendFrom?.applyTo(state, measurables)
         applyToState(state)
     }
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-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt
index a439827..ba68ca1 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt
@@ -260,6 +260,7 @@
             ItemHolder(i, slotPrefix, showSlots) {
                 if (visible) {
                     if (provider.value.hasItemsWithProperties()) {
+                        @Suppress("DEPRECATION")
                         val properties = motionProperties("$slotPrefix$i")
                         provider.value.getContent(idx, properties).invoke()
                     } else {
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionDragHandler.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionDragHandler.kt
index 72f65ca..5f2cccd 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionDragHandler.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionDragHandler.kt
@@ -42,7 +42,7 @@
 @PublishedApi
 @ExperimentalMotionApi
 internal inline fun Modifier.motionPointerInput(
-    key: Any = Unit,
+    key: Any,
     motionProgress: MotionProgress,
     measurer: MotionMeasurer
 ): Modifier = composed(
@@ -67,10 +67,8 @@
             if (isTouchUp && swipeHandler.pendingProgressWhileTouchUp()) {
                 // Loop until there's no need to update the progress or the there's a touch down
                 swipeHandler.updateProgressWhileTouchUp()
-                // TODO: Once the progress while Up ends, snap the progress to target (0 or 1)
             } else {
                 if (dragState == null) {
-                    // TODO: Investigate if it's worth skipping some drag events
                     dragState = dragChannel.receive()
                 }
                 coroutineContext.ensureActive()
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt
index e3f663bc..c228b64 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt
@@ -26,6 +26,7 @@
 import androidx.compose.runtime.State
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.neverEqualPolicy
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
@@ -56,12 +57,18 @@
     FullMeasure(1)
 }
 
+enum class MotionLayoutDebugFlags {
+    NONE,
+    SHOW_ALL,
+    UNKNOWN
+}
+
 /**
  * Layout that interpolate its children layout given two sets of constraint and
  * a progress (from 0 to 1)
  */
+@Suppress("UNUSED_PARAMETER")
 @ExperimentalMotionApi
-@Suppress("NOTHING_TO_INLINE")
 @Composable
 inline fun MotionLayout(
     start: ConstraintSet,
@@ -75,16 +82,18 @@
     crossinline content: @Composable MotionLayoutScope.() -> Unit
 ) {
     val motionProgress = createAndUpdateMotionProgress(progress = progress)
+    val showDebug = debug.firstOrNull() == MotionLayoutDebugFlags.SHOW_ALL
     MotionLayoutCore(
         start = start,
         end = end,
         transition = transition as? TransitionImpl,
         motionProgress = motionProgress,
-        debugFlag = debug.firstOrNull() ?: MotionLayoutDebugFlags.NONE,
         informationReceiver = null,
-        modifier = modifier,
         optimizationLevel = optimizationLevel,
-        motionLayoutFlags = motionLayoutFlags,
+        showBounds = showDebug,
+        showPaths = showDebug,
+        showKeyPositions = showDebug,
+        modifier = modifier,
         content = content
     )
 }
@@ -94,7 +103,6 @@
  * 1).
  */
 @ExperimentalMotionApi
-@Suppress("NOTHING_TO_INLINE")
 @Composable
 inline fun MotionLayout(
     motionScene: MotionScene,
@@ -156,7 +164,7 @@
     )
 }
 
-@Suppress("NOTHING_TO_INLINE")
+@Suppress("UNUSED_PARAMETER")
 @ExperimentalMotionApi
 @Composable
 inline fun MotionLayout(
@@ -172,16 +180,18 @@
     crossinline content: @Composable (MotionLayoutScope.() -> Unit)
 ) {
     val motionProgress = createAndUpdateMotionProgress(progress = progress)
+    val showDebug = debug.firstOrNull() == MotionLayoutDebugFlags.SHOW_ALL
     MotionLayoutCore(
         start = start,
         end = end,
         transition = transition as? TransitionImpl,
         motionProgress = motionProgress,
-        debugFlag = debug.firstOrNull() ?: MotionLayoutDebugFlags.NONE,
         informationReceiver = informationReceiver,
-        modifier = modifier,
         optimizationLevel = optimizationLevel,
-        motionLayoutFlags = motionLayoutFlags,
+        showBounds = showDebug,
+        showPaths = showDebug,
+        showKeyPositions = showDebug,
+        modifier = modifier,
         content = content
     )
 }
@@ -189,7 +199,7 @@
 @ExperimentalMotionApi
 @PublishedApi
 @Composable
-@Suppress("UnavailableSymbol")
+@Suppress("UnavailableSymbol", "UNUSED_PARAMETER")
 internal inline fun MotionLayoutCore(
     @Suppress("HiddenTypeParameter")
     motionScene: MotionScene,
@@ -276,11 +286,12 @@
         end = end,
         transition = transition as? TransitionImpl,
         motionProgress = motionProgress,
-        debugFlag = debugFlag,
         informationReceiver = motionScene as? LayoutInformationReceiver,
-        modifier = modifier,
         optimizationLevel = optimizationLevel,
-        motionLayoutFlags = motionLayoutFlags,
+        showBounds = debugFlag == MotionLayoutDebugFlags.SHOW_ALL,
+        showPaths = debugFlag == MotionLayoutDebugFlags.SHOW_ALL,
+        showKeyPositions = debugFlag == MotionLayoutDebugFlags.SHOW_ALL,
+        modifier = modifier,
         content = content
     )
 }
@@ -293,11 +304,11 @@
     @Suppress("HiddenTypeParameter")
     motionScene: MotionScene,
     progress: Float,
-    modifier: Modifier = Modifier,
-    debug: EnumSet<MotionLayoutDebugFlags> = EnumSet.of(MotionLayoutDebugFlags.NONE),
-    optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
     transitionName: String,
-    motionLayoutFlags: Set<MotionLayoutFlag> = setOf<MotionLayoutFlag>(),
+    optimizationLevel: Int,
+    motionLayoutFlags: Set<MotionLayoutFlag>,
+    debug: EnumSet<MotionLayoutDebugFlags>,
+    modifier: Modifier,
     @Suppress("HiddenTypeParameter")
     crossinline content: @Composable MotionLayoutScope.() -> Unit,
 ) {
@@ -332,91 +343,12 @@
 }
 
 @ExperimentalMotionApi
-@PublishedApi
-@Composable
-@Suppress("UnavailableSymbol")
-internal inline fun MotionLayoutCore(
-    start: ConstraintSet,
-    end: ConstraintSet,
-    modifier: Modifier = Modifier,
-    @SuppressWarnings("HiddenTypeParameter") transition: TransitionImpl? = null,
-    motionProgress: MotionProgress,
-    debugFlag: MotionLayoutDebugFlags = MotionLayoutDebugFlags.NONE,
-    informationReceiver: LayoutInformationReceiver? = null,
-    optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
-    motionLayoutFlags: Set<MotionLayoutFlag> = setOf<MotionLayoutFlag>(),
-    @Suppress("HiddenTypeParameter")
-    crossinline content: @Composable MotionLayoutScope.() -> Unit
-) {
-    // TODO: Merge this snippet with UpdateWithForcedIfNoUserChange
-    val needsUpdate = remember { mutableStateOf(0L) }
-    needsUpdate.value // Read the value to allow recomposition from informationReceiver
-    informationReceiver?.setUpdateFlag(needsUpdate)
-
-    UpdateWithForcedIfNoUserChange(
-        motionProgress = motionProgress,
-        informationReceiver = informationReceiver
-    )
-
-    var usedDebugMode = debugFlag
-    val forcedDebug = informationReceiver?.getForcedDrawDebug()
-    if (forcedDebug != null && forcedDebug != MotionLayoutDebugFlags.UNKNOWN) {
-        usedDebugMode = forcedDebug
-    }
-    val density = LocalDensity.current
-    val measurer = remember { MotionMeasurer(density) }
-    val scope = remember { MotionLayoutScope(measurer, motionProgress) }
-    val debug = EnumSet.of(usedDebugMode)
-    val measurePolicy =
-        rememberMotionLayoutMeasurePolicy(
-            optimizationLevel,
-            debug,
-            start,
-            end,
-            transition,
-            motionProgress,
-            motionLayoutFlags,
-            measurer
-        )
-
-    measurer.addLayoutInformationReceiver(informationReceiver)
-
-    val forcedScaleFactor = measurer.forcedScaleFactor
-
-    var debugModifications: Modifier = Modifier
-    if (!debug.contains(MotionLayoutDebugFlags.NONE) || !forcedScaleFactor.isNaN()) {
-        if (!forcedScaleFactor.isNaN()) {
-            debugModifications = debugModifications.scale(forcedScaleFactor)
-        }
-        debugModifications = debugModifications.drawBehind {
-            with(measurer) {
-                if (!forcedScaleFactor.isNaN()) {
-                    drawDebugBounds(forcedScaleFactor)
-                }
-                if (!debug.contains(MotionLayoutDebugFlags.NONE)) {
-                    drawDebug()
-                }
-            }
-        }
-    }
-    @Suppress("DEPRECATION")
-    (MultiMeasureLayout(
-        modifier = modifier
-            .then(debugModifications)
-            .motionPointerInput(measurePolicy, motionProgress, measurer)
-            .semantics { designInfoProvider = measurer },
-        measurePolicy = measurePolicy,
-        content = { scope.content() }
-    ))
-}
-
-@ExperimentalMotionApi
 @Composable
 inline fun MotionLayout(
+    motionScene: MotionScene,
+    motionLayoutState: MotionLayoutState,
     modifier: Modifier = Modifier,
     optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
-    motionLayoutState: MotionLayoutState,
-    motionScene: MotionScene,
     crossinline content: @Composable MotionLayoutScope.() -> Unit
 ) {
     MotionLayoutCore(
@@ -424,6 +356,7 @@
         optimizationLevel = optimizationLevel,
         motionLayoutState = motionLayoutState as MotionLayoutStateImpl,
         motionScene = motionScene,
+        transitionName = "default",
         content = content
     )
 }
@@ -433,12 +366,12 @@
 @Composable
 @Suppress("UnavailableSymbol")
 internal inline fun MotionLayoutCore(
-    modifier: Modifier = Modifier,
-    optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
-    motionLayoutState: MotionLayoutStateImpl,
     @Suppress("HiddenTypeParameter")
     motionScene: MotionScene,
-    transitionName: String = "default",
+    transitionName: String,
+    motionLayoutState: MotionLayoutStateImpl,
+    optimizationLevel: Int,
+    modifier: Modifier,
     @Suppress("HiddenTypeParameter")
     crossinline content: @Composable MotionLayoutScope.() -> Unit
 ) {
@@ -459,28 +392,190 @@
     if (start == null || end == null) {
         return
     }
+    val showDebug = motionLayoutState.debugMode == MotionLayoutDebugFlags.SHOW_ALL
     MotionLayoutCore(
         start = start,
         end = end,
         transition = transition as? TransitionImpl,
         motionProgress = motionLayoutState.motionProgress,
-        debugFlag = motionLayoutState.debugMode,
         informationReceiver = motionScene as? JSONMotionScene,
-        modifier = modifier,
         optimizationLevel = optimizationLevel,
+        showBounds = showDebug,
+        showPaths = showDebug,
+        showKeyPositions = showDebug,
+        modifier = modifier,
         content = content
     )
 }
 
+@ExperimentalMotionApi
+@PublishedApi
+@Composable
+@Suppress("UnavailableSymbol")
+internal inline fun MotionLayoutCore(
+    start: ConstraintSet,
+    end: ConstraintSet,
+    @SuppressWarnings("HiddenTypeParameter") transition: TransitionImpl?,
+    motionProgress: MotionProgress,
+    informationReceiver: LayoutInformationReceiver?,
+    optimizationLevel: Int,
+    showBounds: Boolean,
+    showPaths: Boolean,
+    showKeyPositions: Boolean,
+    modifier: Modifier,
+    @Suppress("HiddenTypeParameter")
+    crossinline content: @Composable MotionLayoutScope.() -> Unit
+) {
+    // TODO: Merge this snippet with UpdateWithForcedIfNoUserChange
+    val needsUpdate = remember { mutableStateOf(0L) }
+    needsUpdate.value // Read the value to allow recomposition from informationReceiver
+    informationReceiver?.setUpdateFlag(needsUpdate)
+
+    UpdateWithForcedIfNoUserChange(
+        motionProgress = motionProgress,
+        informationReceiver = informationReceiver
+    )
+
+    /**
+     * MutableState used to track content recompositions. It's reassigned at the content's
+     * composition scope, so that any function reading it is recomposed with the content.
+     * NeverEqualPolicy is used so that we don't have to assign any particular value to trigger a
+     * State change.
+     */
+    val contentTracker = remember { mutableStateOf(Unit, neverEqualPolicy()) }
+    val compositionSource =
+        remember { Ref<CompositionSource>().apply { value = CompositionSource.Unknown } }
+    val density = LocalDensity.current
+    val layoutDirection = LocalLayoutDirection.current
+    val measurer = remember { MotionMeasurer(density) }
+    val scope = remember { MotionLayoutScope(measurer, motionProgress) }
+
+    remember(start, end, transition) {
+        measurer.initWith(
+            start = start,
+            end = end,
+            layoutDirection = layoutDirection,
+            transition = transition ?: TransitionImpl.EMPTY,
+            progress = motionProgress.currentProgress
+        )
+        true // Remember is required to return a non-Unit value
+    }
+
+    val measurePolicy = motionLayoutMeasurePolicy(
+        contentTracker = contentTracker,
+        compositionSource = compositionSource,
+        constraintSetStart = start,
+        constraintSetEnd = end,
+        transition = transition ?: TransitionImpl.EMPTY,
+        motionProgress = motionProgress,
+        measurer = measurer,
+        optimizationLevel = optimizationLevel
+    )
+
+    measurer.addLayoutInformationReceiver(informationReceiver)
+
+    val forcedDebug = informationReceiver?.getForcedDrawDebug()
+    val forcedScaleFactor = measurer.forcedScaleFactor
+
+    var doShowBounds = showBounds
+    var doShowPaths = showPaths
+    var doShowKeyPositions = showKeyPositions
+
+    if (forcedDebug != null) {
+        doShowBounds = forcedDebug === MotionLayoutDebugFlags.SHOW_ALL
+        doShowPaths = doShowBounds
+        doShowKeyPositions = doShowBounds
+    }
+
+    @Suppress("DEPRECATION")
+    MultiMeasureLayout(
+        modifier = modifier
+            .motionDebug(
+                measurer = measurer,
+                scaleFactor = forcedScaleFactor,
+                showBounds = doShowBounds,
+                showPaths = doShowPaths,
+                showKeyPositions = doShowKeyPositions
+            )
+            .motionPointerInput(
+                key = transition ?: TransitionImpl.EMPTY,
+                motionProgress = motionProgress,
+                measurer = measurer
+            )
+            .semantics { designInfoProvider = measurer },
+        measurePolicy = measurePolicy,
+        content = {
+            // Perform a reassignment to the State tracker, this will force readers to recompose at
+            // the same pass as the content. The only expected reader is our MeasurePolicy.
+            contentTracker.value = Unit
+
+            if (compositionSource.value == CompositionSource.Unknown) {
+                // Set the content as the original composition source if the MotionLayout was not
+                // recomposed by the caller or by itself
+                compositionSource.value = CompositionSource.Content
+            }
+            scope.content()
+        }
+    )
+}
+
 @LayoutScopeMarker
 @ExperimentalMotionApi
 class MotionLayoutScope @Suppress("ShowingMemberInHiddenClass")
-    @PublishedApi internal constructor(
+@PublishedApi internal constructor(
     private val measurer: MotionMeasurer,
     private val motionProgress: MotionProgress
 ) {
 
     @ExperimentalMotionApi
+    inner class CustomProperties internal constructor(private val id: String) {
+        /**
+         * Return the current [Color] value of the custom property [name], of the [id] layout.
+         *
+         * Returns [Color.Unspecified] if the property does not exist.
+         */
+        fun color(name: String): Color {
+            return measurer.getCustomColor(id, name, motionProgress.currentProgress)
+        }
+
+        /**
+         * Return the current [Color] value of the custom property [name], of the [id] layout.
+         *
+         * Returns [Color.Unspecified] if the property does not exist.
+         */
+        fun float(name: String): Float {
+            return measurer.getCustomFloat(id, name, motionProgress.currentProgress)
+        }
+
+        /**
+         * Return the current [Int] value of the custom property [name], of the [id] layout.
+         *
+         * Returns `0` if the property does not exist.
+         */
+        fun int(name: String): Int {
+            return measurer.getCustomFloat(id, name, motionProgress.currentProgress).toInt()
+        }
+
+        /**
+         * Return the current [Dp] value of the custom property [name], of the [id] layout.
+         *
+         * Returns [Dp.Unspecified] if the property does not exist.
+         */
+        fun distance(name: String): Dp {
+            return measurer.getCustomFloat(id, name, motionProgress.currentProgress).dp
+        }
+
+        /**
+         * Return the current [TextUnit] value of the custom property [name], of the [id] layout.
+         *
+         * Returns [TextUnit.Unspecified] if the property does not exist.
+         */
+        fun fontSize(name: String): TextUnit {
+            return measurer.getCustomFloat(id, name, motionProgress.currentProgress).sp
+        }
+    }
+
+    @ExperimentalMotionApi // TODO: Remove for 1.2.0-alphaXX with all dependent functions
     inner class MotionProperties internal constructor(
         id: String,
         tag: String?
@@ -517,6 +612,10 @@
         }
     }
 
+    @Deprecated(
+        "Unnecessary composable, name is also inconsistent for custom properties",
+        ReplaceWith("customProperties(id)")
+    )
     @Composable
     fun motionProperties(id: String): State<MotionProperties> =
     // TODO: There's no point on returning a [State] object, and probably no point on this being
@@ -525,88 +624,149 @@
             mutableStateOf(MotionProperties(id, null))
         }
 
+    @Deprecated("Deprecated for naming consistency", ReplaceWith("customProperties(id)"))
     fun motionProperties(id: String, tag: String): MotionProperties {
         return MotionProperties(id, tag)
     }
 
+    @Deprecated("Deprecated for naming consistency", ReplaceWith("customColor(id, name)"))
     fun motionColor(id: String, name: String): Color {
         return measurer.getCustomColor(id, name, motionProgress.currentProgress)
     }
 
+    @Deprecated("Deprecated for naming consistency", ReplaceWith("customFloat(id, name)"))
     fun motionFloat(id: String, name: String): Float {
         return measurer.getCustomFloat(id, name, motionProgress.currentProgress)
     }
 
+    @Deprecated("Deprecated for naming consistency", ReplaceWith("customInt(id, name)"))
     fun motionInt(id: String, name: String): Int {
         return measurer.getCustomFloat(id, name, motionProgress.currentProgress).toInt()
     }
 
+    @Deprecated("Deprecated for naming consistency", ReplaceWith("customDistance(id, name)"))
     fun motionDistance(id: String, name: String): Dp {
         return measurer.getCustomFloat(id, name, motionProgress.currentProgress).dp
     }
 
+    @Deprecated("Deprecated for naming consistency", ReplaceWith("customFontSize(id, name)"))
     fun motionFontSize(id: String, name: String): TextUnit {
         return measurer.getCustomFloat(id, name, motionProgress.currentProgress).sp
     }
+
+    /**
+     * Returns a [CustomProperties] instance to access the values of custom properties defined for
+     * [id] in different return types: Color, Float, Int, Dp, TextUnit.
+     *
+     * &nbsp;
+     *
+     * Note that there are no type guarantees when setting or getting custom properties, so be
+     * mindful of the value type used for it in the MotionScene.
+     */
+    fun customProperties(id: String): CustomProperties = CustomProperties(id)
+
+    /**
+     * Return the current [Color] value of the custom property [name], of the [id] layout.
+     *
+     * Returns [Color.Unspecified] if the property does not exist.
+     *
+     * &nbsp;
+     *
+     * This is a short version of: `customProperties(id).color(name)`.
+     */
+    fun customColor(id: String, name: String): Color {
+        return measurer.getCustomColor(id, name, motionProgress.currentProgress)
+    }
+
+    /**
+     * Return the current [Color] value of the custom property [name], of the [id] layout.
+     *
+     * Returns [Color.Unspecified] if the property does not exist.
+     *
+     * &nbsp;
+     *
+     * This is a short version of: `customProperties(id).float(name)`.
+     */
+    fun customFloat(id: String, name: String): Float {
+        return measurer.getCustomFloat(id, name, motionProgress.currentProgress)
+    }
+
+    /**
+     * Return the current [Int] value of the custom property [name], of the [id] layout.
+     *
+     * Returns `0` if the property does not exist.
+     *
+     * &nbsp;
+     *
+     * This is a short version of: `customProperties(id).int(name)`.
+     */
+    fun customInt(id: String, name: String): Int {
+        return measurer.getCustomFloat(id, name, motionProgress.currentProgress).toInt()
+    }
+
+    /**
+     * Return the current [Dp] value of the custom property [name], of the [id] layout.
+     *
+     * Returns [Dp.Unspecified] if the property does not exist.
+     *
+     * &nbsp;
+     *
+     * This is a short version of: `customProperties(id).distance(name)`.
+     */
+    fun customDistance(id: String, name: String): Dp {
+        return measurer.getCustomFloat(id, name, motionProgress.currentProgress).dp
+    }
+
+    /**
+     * Return the current [TextUnit] value of the custom property [name], of the [id] layout.
+     *
+     * Returns [TextUnit.Unspecified] if the property does not exist.
+     *
+     * &nbsp;
+     *
+     * This is a short version of: `customProperties(id).fontSize(name)`.
+     */
+    fun customFontSize(id: String, name: String): TextUnit {
+        return measurer.getCustomFloat(id, name, motionProgress.currentProgress).sp
+    }
 }
 
-enum class MotionLayoutDebugFlags {
-    NONE,
-    SHOW_ALL,
-    UNKNOWN
-}
-
-@Composable
 @PublishedApi
 @ExperimentalMotionApi
-internal fun rememberMotionLayoutMeasurePolicy(
-    optimizationLevel: Int,
-    debug: EnumSet<MotionLayoutDebugFlags>,
+internal fun motionLayoutMeasurePolicy(
+    contentTracker: State<Unit>,
+    compositionSource: Ref<CompositionSource>,
     constraintSetStart: ConstraintSet,
     constraintSetEnd: ConstraintSet,
-    @SuppressWarnings("HiddenTypeParameter") transition: TransitionImpl?,
+    @SuppressWarnings("HiddenTypeParameter") transition: TransitionImpl,
     motionProgress: MotionProgress,
-    motionLayoutFlags: Set<MotionLayoutFlag> = setOf<MotionLayoutFlag>(),
-    measurer: MotionMeasurer
-): MeasurePolicy {
-    val density = LocalDensity.current
-    val layoutDirection = LocalLayoutDirection.current
-    return remember(
-        optimizationLevel,
-        motionLayoutFlags,
-        debug,
-        constraintSetStart,
-        constraintSetEnd,
-        transition
-    ) {
-        measurer.initWith(
+    measurer: MotionMeasurer,
+    optimizationLevel: Int,
+): MeasurePolicy =
+    MeasurePolicy { measurables, constraints ->
+        // Do a state read, to guarantee that we control measure when the content recomposes without
+        // notifying our Composable caller
+        contentTracker.value
+
+        val layoutSize = measurer.performInterpolationMeasure(
+            constraints,
+            this.layoutDirection,
             constraintSetStart,
             constraintSetEnd,
-            density,
-            layoutDirection,
             transition,
-            motionProgress.currentProgress
+            measurables,
+            optimizationLevel,
+            motionProgress.currentProgress,
+            compositionSource.value ?: CompositionSource.Unknown
         )
-        MeasurePolicy { measurables, constraints ->
-            val layoutSize = measurer.performInterpolationMeasure(
-                constraints,
-                layoutDirection,
-                constraintSetStart,
-                constraintSetEnd,
-                transition,
-                measurables,
-                optimizationLevel,
-                motionProgress.currentProgress,
-                motionLayoutFlags
-            )
-            layout(layoutSize.width, layoutSize.height) {
-                with(measurer) {
-                    performLayout(measurables)
-                }
+        compositionSource.value = CompositionSource.Unknown // Reset after measuring
+
+        layout(layoutSize.width, layoutSize.height) {
+            with(measurer) {
+                performLayout(measurables)
             }
         }
     }
-}
 
 /**
  * Updates [motionProgress] from changes in [LayoutInformationReceiver.getForcedProgress].
@@ -644,10 +804,9 @@
  * @param progress User progress, if changed, updates the underlying [MotionProgress]
  * @return A [MotionProgress] instance that may change from internal or external calls
  */
-@Suppress("NOTHING_TO_INLINE")
 @PublishedApi
 @Composable
-internal inline fun createAndUpdateMotionProgress(progress: Float): MotionProgress {
+internal fun createAndUpdateMotionProgress(progress: Float): MotionProgress {
     val motionProgress = remember {
         MotionProgress.fromMutableState(mutableStateOf(progress))
     }
@@ -658,4 +817,52 @@
         motionProgress.updateProgress(progress)
     }
     return motionProgress
+}
+
+@PublishedApi
+@ExperimentalMotionApi
+internal fun Modifier.motionDebug(
+    measurer: MotionMeasurer,
+    scaleFactor: Float,
+    showBounds: Boolean,
+    showPaths: Boolean,
+    showKeyPositions: Boolean
+): Modifier {
+    var debugModifier: Modifier = this
+    if (!scaleFactor.isNaN()) {
+        debugModifier = debugModifier.scale(scaleFactor)
+    }
+    if (showBounds || showKeyPositions || showPaths) {
+        debugModifier = debugModifier.drawBehind {
+            with(measurer) {
+                drawDebug(
+                    drawBounds = showBounds,
+                    drawPaths = showPaths,
+                    drawKeyPositions = showKeyPositions
+                )
+            }
+        }
+    }
+    return debugModifier
+}
+
+/**
+ * Indicates where the composition was initiated.
+ *
+ * The source will help us identify possible pathways for optimization.
+ *
+ * E.g.: If the content was not recomposed, we can assume that previous measurements are still valid,
+ * so there's no need to recalculate the entire interpolation, only the current frame.
+ */
+@PublishedApi
+internal enum class CompositionSource {
+    // TODO: Add an explicit option for Composition initiated internally
+
+    Unknown,
+
+    /**
+     * Content recomposed, need to remeasure everything: **start**, **end** and **interpolated**
+     * states.
+     */
+    Content
 }
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionMeasurer.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionMeasurer.kt
index 3078295..e768017 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionMeasurer.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionMeasurer.kt
@@ -33,7 +33,6 @@
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastAny
 import androidx.compose.ui.util.fastForEach
 import androidx.constraintlayout.core.motion.Motion
 import androidx.constraintlayout.core.state.Dimension
@@ -59,6 +58,7 @@
     ) {
         state.reset()
         constraintSet.applyTo(state, measurables)
+        buildMapping(state, measurables)
         state.apply(root)
         root.children.fastForEach { it.isAnimated = true }
         applyRootSize(constraints)
@@ -83,19 +83,13 @@
         layoutDirection: LayoutDirection,
         constraintSetStart: ConstraintSet,
         constraintSetEnd: ConstraintSet,
-        @SuppressWarnings("HiddenTypeParameter") transition: TransitionImpl?,
+        @SuppressWarnings("HiddenTypeParameter") transition: TransitionImpl,
         measurables: List<Measurable>,
         optimizationLevel: Int,
         progress: Float,
-        motionLayoutFlags: Set<MotionLayoutFlag> = setOf<MotionLayoutFlag>()
+        compositionSource: CompositionSource
     ): IntSize {
-        var needsRemeasure = false
-        var flag = motionLayoutFlags.firstOrNull()
-        if (flag == MotionLayoutFlag.Default || flag == null) {
-            needsRemeasure = needsRemeasure(constraints)
-        } else if (flag == MotionLayoutFlag.FullMeasure) {
-            needsRemeasure = true
-        }
+        val needsRemeasure = needsRemeasure(constraints, compositionSource)
 
         if (lastProgressInInterpolation != progress ||
             (layoutInformationReceiver?.getForcedWidth() != Int.MIN_VALUE &&
@@ -125,7 +119,7 @@
      * MotionLayout size might change from its parent Layout, and in some cases the children size
      * might change (eg: A Text layout has a longer string appended).
      */
-    private fun needsRemeasure(constraints: Constraints): Boolean {
+    private fun needsRemeasure(constraints: Constraints, source: CompositionSource): Boolean {
         if (this.transition.isEmpty || frameCache.isEmpty()) {
             // Nothing measured (by MotionMeasurer)
             return true
@@ -138,18 +132,8 @@
             return true
         }
 
-        return root.children.fastAny { child ->
-            // Check if measurables have changed their size
-            val measurable = (child.companionWidget as? Measurable) ?: return@fastAny false
-            val interpolatedFrame = this.transition.getInterpolated(child) ?: return@fastAny false
-            val placeable = placeables[measurable] ?: return@fastAny false
-            val currentWidth = placeable.width
-            val currentHeight = placeable.height
-
-            // Need to recalculate interpolation if the size of any element changed
-            return@fastAny currentWidth != interpolatedFrame.width() ||
-                currentHeight != interpolatedFrame.height()
-        }
+        // Content recomposed
+        return source == CompositionSource.Content
     }
 
     /**
@@ -174,7 +158,6 @@
         if (remeasure) {
             this.transition.clear()
             resetMeasureState()
-            state.reset()
             // Define the size of the ConstraintLayout.
             state.width(
                 if (constraints.hasFixedWidth) {
@@ -203,28 +186,24 @@
             )
             this.transition.updateFrom(root, Transition.END)
             transition?.applyKeyFramesTo(this.transition)
+        } else {
+            // Have to remap even if there's no reason to remeasure
+            buildMapping(state, measurables)
         }
-
         this.transition.interpolate(root.width, root.height, progress)
         root.width = this.transition.interpolatedWidth
         root.height = this.transition.interpolatedHeight
+        // Update measurables to interpolated dimensions
         root.children.fastForEach { child ->
             // Update measurables to the interpolated dimensions
             val measurable = (child.companionWidget as? Measurable) ?: return@fastForEach
             val interpolatedFrame = this.transition.getInterpolated(child) ?: return@fastForEach
-            val placeable = placeables[measurable]
-            val currentWidth = placeable?.width
-            val currentHeight = placeable?.height
-            if (placeable == null ||
-                currentWidth != interpolatedFrame.width() ||
-                currentHeight != interpolatedFrame.height()
-            ) {
-                measurable.measure(
-                    Constraints.fixed(interpolatedFrame.width(), interpolatedFrame.height())
-                ).also { newPlaceable ->
-                    placeables[measurable] = newPlaceable
-                }
-            }
+            placeables[measurable] = measurable.measure(
+                Constraints.fixed(
+                    interpolatedFrame.width(),
+                    interpolatedFrame.height()
+                )
+            )
             frameCache[measurable] = interpolatedFrame
         }
 
@@ -310,34 +289,61 @@
         layoutInformationReceiver?.setLayoutInformation(json.toString())
     }
 
-    fun DrawScope.drawDebug() {
-        var index = 0
+    /**
+     * Draws debug information related to the current Transition.
+     *
+     * Typically, this means drawing the bounds of each widget at the start/end positions, the path
+     * they take and indicators for KeyPositions.
+     */
+    fun DrawScope.drawDebug(
+        drawBounds: Boolean = true,
+        drawPaths: Boolean = true,
+        drawKeyPositions: Boolean = true,
+    ) {
         val pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
+
         for (child in root.children) {
             val startFrame = transition.getStart(child)
             val endFrame = transition.getEnd(child)
-            translate(2f, 2f) {
-                drawFrameDebug(
-                    size.width,
-                    size.height,
-                    startFrame,
-                    endFrame,
-                    pathEffect,
-                    Color.White
-                )
+            if (drawBounds) {
+                // Draw widget bounds at the start and end
+                drawFrame(frame = startFrame, pathEffect = pathEffect, color = Color.Blue)
+                drawFrame(frame = endFrame, pathEffect = pathEffect, color = Color.Blue)
+                translate(2f, 2f) {
+                    // Do an additional offset draw in case the bounds are not visible/obstructed
+                    drawFrame(frame = startFrame, pathEffect = pathEffect, color = Color.White)
+                    drawFrame(frame = endFrame, pathEffect = pathEffect, color = Color.White)
+                }
             }
-            drawFrameDebug(
-                size.width,
-                size.height,
-                startFrame,
-                endFrame,
-                pathEffect,
-                Color.Blue
+            drawPaths(
+                parentWidth = size.width,
+                parentHeight = size.height,
+                startFrame = startFrame,
+                drawPath = drawPaths,
+                drawKeyPositions = drawKeyPositions
             )
-            index++
         }
     }
 
+    private fun DrawScope.drawPaths(
+        parentWidth: Float,
+        parentHeight: Float,
+        startFrame: WidgetFrame,
+        drawPath: Boolean,
+        drawKeyPositions: Boolean
+    ) {
+        val debugRender = MotionRenderDebug(23f)
+        debugRender.basicDraw(
+            drawContext.canvas.nativeCanvas,
+            transition.getMotion(startFrame.widget.stringId),
+            1000,
+            parentWidth.toInt(),
+            parentHeight.toInt(),
+            drawPath,
+            drawKeyPositions
+        )
+    }
+
     private fun DrawScope.drawFrameDebug(
         parentWidth: Float,
         parentHeight: Float,
@@ -475,17 +481,20 @@
      * ConstraintWidget corresponding to [id], the value is calculated at the given [progress] value
      * on the current Transition.
      *
-     * Returns [Color.Black] if the custom property doesn't exist.
+     * Returns [Color.Unspecified] if the custom property doesn't exist.
      */
     fun getCustomColor(id: String, name: String, progress: Float): Color {
         if (!transition.contains(id)) {
-            return Color.Black
+            return Color.Unspecified
         }
         transition.interpolate(root.width, root.height, progress)
 
         val interpolatedFrame = transition.getInterpolated(id)
-        val color = interpolatedFrame.getCustomColor(name)
-        return Color(color)
+
+        if (!interpolatedFrame.containsCustom(name)) {
+            return Color.Unspecified
+        }
+        return Color(interpolatedFrame.getCustomColor(name))
     }
 
     /**
@@ -493,13 +502,14 @@
      * ConstraintWidget corresponding to [id], the value is calculated at the given [progress] value
      * on the current Transition.
      *
-     * Returns `0f` if the custom property doesn't exist.
+     * Returns [Float.NaN] if the custom property doesn't exist.
      */
     fun getCustomFloat(id: String, name: String, progress: Float): Float {
         if (!transition.contains(id)) {
-            return 0f
+            return Float.NaN
         }
         transition.interpolate(root.width, root.height, progress)
+
         val interpolatedFrame = transition.getInterpolated(id)
         return interpolatedFrame.getCustomFloat(name)
     }
@@ -513,28 +523,24 @@
     fun initWith(
         start: ConstraintSet,
         end: ConstraintSet,
-        density: Density,
         layoutDirection: LayoutDirection,
-        @SuppressWarnings("HiddenTypeParameter") transition: TransitionImpl?,
+        @SuppressWarnings("HiddenTypeParameter") transition: TransitionImpl,
         progress: Float
     ) {
         clearConstraintSets()
 
-        // FIXME: tempState is a hack to populate initial custom properties with DSL
-        val tempState = State(density).apply {
-            this.isLtr = layoutDirection == LayoutDirection.Ltr
-        }
-        start.applyTo(tempState, emptyList())
+        state.isLtr = layoutDirection == LayoutDirection.Ltr
+        start.applyTo(state, emptyList())
         start.applyTo(this.transition, Transition.START)
-        tempState.apply(root)
+        state.apply(root)
         this.transition.updateFrom(root, Transition.START)
 
-        start.applyTo(tempState, emptyList())
+        start.applyTo(state, emptyList())
         end.applyTo(this.transition, Transition.END)
-        tempState.apply(root)
+        state.apply(root)
         this.transition.updateFrom(root, Transition.END)
 
         this.transition.interpolate(0, 0, progress)
-        transition?.applyAllTo(this.transition)
+        transition.applyAllTo(this.transition)
     }
 }
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionRenderDebug.java b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionRenderDebug.java
index 4302c7c..e259358 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionRenderDebug.java
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionRenderDebug.java
@@ -21,6 +21,7 @@
 import android.graphics.Path;
 import android.graphics.Rect;
 
+import androidx.annotation.NonNull;
 import androidx.constraintlayout.core.motion.Motion;
 import androidx.constraintlayout.core.motion.MotionPaths;
 
@@ -170,6 +171,63 @@
         drawTicks(canvas, mode, keyFrames, motionController, layoutWidth, layoutHeight);
     }
 
+
+    /**
+     * Draws the paths of the given {@link Motion motionController}, forcing the drawing mode
+     * {@link Motion#DRAW_PATH_BASIC}.
+     *
+     * @param canvas Canvas instance used to draw on
+     * @param motionController Controller containing path information
+     * @param duration Defined in milliseconds, sets the amount of ticks used to draw the path
+     *                 based on {@link #DEBUG_PATH_TICKS_PER_MS}
+     * @param layoutWidth Width of the containing MotionLayout
+     * @param layoutHeight Height of the containing MotionLayout
+     * @param drawPath Whether to draw the path, paths are drawn using dashed lines
+     * @param drawTicks Whether to draw diamond shaped ticks that indicate KeyPositions along a path
+     */
+    void basicDraw(@NonNull Canvas canvas,
+            @NonNull Motion motionController,
+            int duration,
+            int layoutWidth,
+            int layoutHeight,
+            boolean drawPath,
+            boolean drawTicks) {
+        int mode = Motion.DRAW_PATH_BASIC;
+        mKeyFrameCount = motionController.buildKeyFrames(mKeyFramePoints, mPathMode, null);
+
+        int frames = duration / DEBUG_PATH_TICKS_PER_MS;
+        if (mPoints == null || mPoints.length != frames * 2) {
+            mPoints = new float[frames * 2];
+            mPath = new Path();
+        }
+
+        canvas.translate(mShadowTranslate, mShadowTranslate);
+
+        mPaint.setColor(mShadowColor);
+        mFillPaint.setColor(mShadowColor);
+        mPaintKeyframes.setColor(mShadowColor);
+        mPaintGraph.setColor(mShadowColor);
+        motionController.buildPath(mPoints, frames);
+        if (drawPath) {
+            drawBasicPath(canvas);
+        }
+        if (drawTicks) {
+            drawTicks(canvas, mode, mKeyFrameCount, motionController, layoutWidth, layoutHeight);
+        }
+
+        mPaint.setColor(mRedColor);
+        mPaintKeyframes.setColor(mKeyframeColor);
+        mFillPaint.setColor(mKeyframeColor);
+        mPaintGraph.setColor(mGraphColor);
+        canvas.translate(-mShadowTranslate, -mShadowTranslate);
+        if (drawPath) {
+            drawBasicPath(canvas);
+        }
+        if (drawTicks) {
+            drawTicks(canvas, mode, mKeyFrameCount, motionController, layoutWidth, layoutHeight);
+        }
+    }
+
     private void drawBasicPath(Canvas canvas) {
         canvas.drawLines(mPoints, mPaint);
     }
@@ -376,5 +434,4 @@
         mPaint.setColor(0xFFFF0000);
         canvas.drawPath(mPath, mPaint);
     }
-
-}
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionSceneScope.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionSceneScope.kt
index 35287d9..0b17e86 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionSceneScope.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionSceneScope.kt
@@ -126,11 +126,18 @@
     private var generatedCount = 0
 
     /**
+     * Count of generated ConstraintLayoutReference IDs.
+     */
+    private var generatedIdCount = 0
+
+    /**
      * Returns a new unique name. Should be used when the user does not provide a specific name
      * for their ConstraintSets/Transitions.
      */
     private fun nextName() = UNDEFINED_NAME_PREFIX + generatedCount++
 
+    private fun nextId() = UNDEFINED_NAME_PREFIX + "id${generatedIdCount++}"
+
     internal var constraintSetsByName = HashMap<String, ConstraintSet>()
     internal var transitionsByName = HashMap<String, Transition>()
 
@@ -243,6 +250,75 @@
     fun createRefFor(id: Any): ConstrainedLayoutReference = ConstrainedLayoutReference(id)
 
     /**
+     * Convenient way to create multiple [ConstrainedLayoutReference] with one statement, the [ids]
+     * provided should match Composables within ConstraintLayout using [androidx.compose.ui.Modifier.layoutId].
+     *
+     * Example:
+     * ```
+     * val (box, text, button) = createRefsFor("box", "text", "button")
+     * ```
+     * Note that the number of ids should match the number of variables assigned.
+     *
+     * &nbsp;
+     *
+     * To create a singular [ConstrainedLayoutReference] see [createRefFor].
+     */
+    fun createRefsFor(vararg ids: Any): ConstrainedLayoutReferences =
+        ConstrainedLayoutReferences(arrayOf(*ids))
+
+    inner class ConstrainedLayoutReferences internal constructor(
+        private val ids: Array<Any>
+    ) {
+        operator fun component1(): ConstrainedLayoutReference =
+            ConstrainedLayoutReference(ids.getOrElse(0) { nextId() })
+
+        operator fun component2(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(1) { nextId() })
+
+        operator fun component3(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(2) { nextId() })
+
+        operator fun component4(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(3) { nextId() })
+
+        operator fun component5(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(4) { nextId() })
+
+        operator fun component6(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(5) { nextId() })
+
+        operator fun component7(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(6) { nextId() })
+
+        operator fun component8(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(7) { nextId() })
+
+        operator fun component9(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(8) { nextId() })
+
+        operator fun component10(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(9) { nextId() })
+
+        operator fun component11(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(10) { nextId() })
+
+        operator fun component12(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(11) { nextId() })
+
+        operator fun component13(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(12) { nextId() })
+
+        operator fun component14(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(13) { nextId() })
+
+        operator fun component15(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(14) { nextId() })
+
+        operator fun component16(): ConstrainedLayoutReference =
+            createRefFor(ids.getOrElse(15) { nextId() })
+    }
+
+    /**
      * Declare a custom Float [value] addressed by [name].
      */
     fun ConstrainScope.customFloat(name: String, value: Float) {
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Transition.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Transition.kt
index 131c043..2c69672 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Transition.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/Transition.kt
@@ -58,6 +58,7 @@
  * Used to reduced the exposed API from [Transition].
  */
 @ExperimentalMotionApi
+@PublishedApi
 internal class TransitionImpl(
     private val parsedTransition: CLObject
 ) : Transition {
@@ -108,7 +109,9 @@
         return parsedTransition.hashCode()
     }
 
+    @PublishedApi
     internal companion object {
+        @PublishedApi
         internal val EMPTY = TransitionImpl(CLObject(charArrayOf()))
     }
 }
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt
index bc1adf0..fdc6605 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt
@@ -401,6 +401,8 @@
         val StartVertical = Arc("startVertical")
         val StartHorizontal = Arc("startHorizontal")
         val Flip = Arc("flip")
+        val Below = Arc("below")
+        val Above = Arc("above")
     }
 }
 
@@ -510,4 +512,4 @@
         val Path: RelativePosition = RelativePosition("pathRelative")
         val Parent: RelativePosition = RelativePosition("parentRelative")
     }
-}
\ No newline at end of file
+}
diff --git a/constraintlayout/constraintlayout-core/api/current.txt b/constraintlayout/constraintlayout-core/api/current.txt
index d7b2374..8eee998 100644
--- a/constraintlayout/constraintlayout-core/api/current.txt
+++ b/constraintlayout/constraintlayout-core/api/current.txt
@@ -1250,6 +1250,8 @@
     method public void getSlope(double, double[]!);
     method public double getSlope(double, int);
     method public double[]! getTimePoints();
+    field public static final int ARC_ABOVE = 5; // 0x5
+    field public static final int ARC_BELOW = 4; // 0x4
     field public static final int ARC_START_FLIP = 3; // 0x3
     field public static final int ARC_START_HORIZONTAL = 2; // 0x2
     field public static final int ARC_START_LINEAR = 0; // 0x0
@@ -2299,6 +2301,7 @@
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BASELINE_TO_BASELINE;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BASELINE_TO_BOTTOM;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BASELINE_TO_TOP;
+    enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BOTTOM_TO_BASELINE;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BOTTOM_TO_BOTTOM;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BOTTOM_TO_TOP;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint CENTER_HORIZONTALLY;
@@ -2312,6 +2315,7 @@
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint RIGHT_TO_RIGHT;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint START_TO_END;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint START_TO_START;
+    enum_constant public static final androidx.constraintlayout.core.state.State.Constraint TOP_TO_BASELINE;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint TOP_TO_BOTTOM;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint TOP_TO_TOP;
   }
@@ -2427,6 +2431,7 @@
     method public void addCustomFloat(String!, float);
     method public float centerX();
     method public float centerY();
+    method public boolean containsCustom(String);
     method public androidx.constraintlayout.core.motion.CustomVariable! getCustomAttribute(String!);
     method public java.util.Set<java.lang.String!>! getCustomAttributeNames();
     method public int getCustomColor(String!);
@@ -2594,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();
@@ -2603,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 d7b2374..8eee998 100644
--- a/constraintlayout/constraintlayout-core/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-core/api/public_plus_experimental_current.txt
@@ -1250,6 +1250,8 @@
     method public void getSlope(double, double[]!);
     method public double getSlope(double, int);
     method public double[]! getTimePoints();
+    field public static final int ARC_ABOVE = 5; // 0x5
+    field public static final int ARC_BELOW = 4; // 0x4
     field public static final int ARC_START_FLIP = 3; // 0x3
     field public static final int ARC_START_HORIZONTAL = 2; // 0x2
     field public static final int ARC_START_LINEAR = 0; // 0x0
@@ -2299,6 +2301,7 @@
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BASELINE_TO_BASELINE;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BASELINE_TO_BOTTOM;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BASELINE_TO_TOP;
+    enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BOTTOM_TO_BASELINE;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BOTTOM_TO_BOTTOM;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BOTTOM_TO_TOP;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint CENTER_HORIZONTALLY;
@@ -2312,6 +2315,7 @@
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint RIGHT_TO_RIGHT;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint START_TO_END;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint START_TO_START;
+    enum_constant public static final androidx.constraintlayout.core.state.State.Constraint TOP_TO_BASELINE;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint TOP_TO_BOTTOM;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint TOP_TO_TOP;
   }
@@ -2427,6 +2431,7 @@
     method public void addCustomFloat(String!, float);
     method public float centerX();
     method public float centerY();
+    method public boolean containsCustom(String);
     method public androidx.constraintlayout.core.motion.CustomVariable! getCustomAttribute(String!);
     method public java.util.Set<java.lang.String!>! getCustomAttributeNames();
     method public int getCustomColor(String!);
@@ -2594,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();
@@ -2603,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 0bdeacc..a7d6598 100644
--- a/constraintlayout/constraintlayout-core/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout-core/api/restricted_current.txt
@@ -1250,6 +1250,8 @@
     method public void getSlope(double, double[]!);
     method public double getSlope(double, int);
     method public double[]! getTimePoints();
+    field public static final int ARC_ABOVE = 5; // 0x5
+    field public static final int ARC_BELOW = 4; // 0x4
     field public static final int ARC_START_FLIP = 3; // 0x3
     field public static final int ARC_START_HORIZONTAL = 2; // 0x2
     field public static final int ARC_START_LINEAR = 0; // 0x0
@@ -2300,6 +2302,7 @@
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BASELINE_TO_BASELINE;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BASELINE_TO_BOTTOM;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BASELINE_TO_TOP;
+    enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BOTTOM_TO_BASELINE;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BOTTOM_TO_BOTTOM;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint BOTTOM_TO_TOP;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint CENTER_HORIZONTALLY;
@@ -2313,6 +2316,7 @@
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint RIGHT_TO_RIGHT;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint START_TO_END;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint START_TO_START;
+    enum_constant public static final androidx.constraintlayout.core.state.State.Constraint TOP_TO_BASELINE;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint TOP_TO_BOTTOM;
     enum_constant public static final androidx.constraintlayout.core.state.State.Constraint TOP_TO_TOP;
   }
@@ -2429,6 +2433,7 @@
     method public void addCustomFloat(String!, float);
     method public float centerX();
     method public float centerY();
+    method public boolean containsCustom(String);
     method public androidx.constraintlayout.core.motion.CustomVariable! getCustomAttribute(String!);
     method public java.util.Set<java.lang.String!>! getCustomAttributeNames();
     method public int getCustomColor(String!);
@@ -2597,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();
@@ -2606,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/motion/MotionPaths.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/MotionPaths.java
index a802192..1b7ad89 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/MotionPaths.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/MotionPaths.java
@@ -28,8 +28,6 @@
 /**
  * This is used to capture and play back path of the layout.
  * It is used to set the bounds of the view (view.layout(l, t, r, b))
- *
- *
  */
 public class MotionPaths implements Comparable<MotionPaths> {
     public static final String TAG = "MotionPaths";
@@ -68,7 +66,6 @@
     int mAnimateCircleAngleTo; // since angles loop there are 4 ways we can pic direction
 
     public MotionPaths() {
-
     }
 
     /**
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/ArcCurveFit.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/ArcCurveFit.java
index bf1a4c0..7df5ef9 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/ArcCurveFit.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/motion/utils/ArcCurveFit.java
@@ -27,11 +27,17 @@
     public static final int ARC_START_VERTICAL = 1;
     public static final int ARC_START_HORIZONTAL = 2;
     public static final int ARC_START_FLIP = 3;
+    public static final int ARC_BELOW = 4;
+    public static final int ARC_ABOVE = 5;
+
     public static final int ARC_START_LINEAR = 0;
 
     private static final int START_VERTICAL = 1;
     private static final int START_HORIZONTAL = 2;
     private static final int START_LINEAR = 3;
+    private static final int DOWN_ARC = 4;
+    private static final int UP_ARC = 5;
+
     private final double[] mTime;
     Arc[] mArcs;
     private boolean mExtrapolate = true;
@@ -275,6 +281,13 @@
                     break;
                 case ARC_START_LINEAR:
                     mode = START_LINEAR;
+                    break;
+                case ARC_ABOVE:
+                    mode = UP_ARC;
+                    break;
+                case ARC_BELOW:
+                    mode = DOWN_ARC;
+                    break;
             }
             mArcs[i] =
                     new Arc(mode, time[i], time[i + 1], y[i][0], y[i][1], y[i + 1][0], y[i + 1][1]);
@@ -302,15 +315,29 @@
         private static final double EPSILON = 0.001;
 
         Arc(int mode, double t1, double t2, double x1, double y1, double x2, double y2) {
-            mVertical = mode == START_VERTICAL;
+            double dx = x2 - x1;
+            double dy = y2 - y1;
+            switch (mode) {
+                case START_VERTICAL:
+                    mVertical = true;
+                    break;
+                case UP_ARC:
+                    mVertical = dy < 0;
+                    break;
+                case DOWN_ARC:
+                    mVertical = dy > 0;
+                    break;
+                default:
+                    mVertical = false;
+            }
+
             mTime1 = t1;
             mTime2 = t2;
             mOneOverDeltaTime = 1 / (mTime2 - mTime1);
             if (START_LINEAR == mode) {
                 mLinear = true;
             }
-            double dx = x2 - x1;
-            double dy = y2 - y1;
+
             if (mLinear || Math.abs(dx) < EPSILON || Math.abs(dy) < EPSILON) {
                 mLinear = true;
                 mX1 = x1;
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/ConstraintReference.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintReference.java
index 4a5a852..53e09a9 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintReference.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/ConstraintReference.java
@@ -20,6 +20,7 @@
 import static androidx.constraintlayout.core.widgets.ConstraintWidget.UNKNOWN;
 import static androidx.constraintlayout.core.widgets.ConstraintWidget.VERTICAL;
 
+import androidx.annotation.Nullable;
 import androidx.constraintlayout.core.motion.utils.TypedBundle;
 import androidx.constraintlayout.core.motion.utils.TypedValues;
 import androidx.constraintlayout.core.state.helpers.Facade;
@@ -116,8 +117,10 @@
     protected Object mEndToEnd = null;
     protected Object mTopToTop = null;
     protected Object mTopToBottom = null;
+    @Nullable Object mTopToBaseline = null;
     protected Object mBottomToTop = null;
     protected Object mBottomToBottom = null;
+    @Nullable Object mBottomToBaseline = null;
     Object mBaselineToBaseline = null;
     Object mBaselineToTop = null;
     Object mBaselineToBottom = null;
@@ -585,6 +588,12 @@
         return this;
     }
 
+    ConstraintReference topToBaseline(Object reference) {
+        mLast = State.Constraint.TOP_TO_BASELINE;
+        mTopToBaseline = reference;
+        return this;
+    }
+
     // @TODO: add description
     public ConstraintReference bottomToTop(Object reference) {
         mLast = State.Constraint.BOTTOM_TO_TOP;
@@ -599,6 +608,12 @@
         return this;
     }
 
+    ConstraintReference bottomToBaseline(Object reference) {
+        mLast = State.Constraint.BOTTOM_TO_BASELINE;
+        mBottomToBaseline = reference;
+        return this;
+    }
+
     // @TODO: add description
     public ConstraintReference baselineToBaseline(Object reference) {
         mLast = State.Constraint.BASELINE_TO_BASELINE;
@@ -715,12 +730,14 @@
                 }
                 break;
                 case TOP_TO_TOP:
-                case TOP_TO_BOTTOM: {
+                case TOP_TO_BOTTOM:
+                case TOP_TO_BASELINE: {
                     mMarginTop = value;
                 }
                 break;
                 case BOTTOM_TO_TOP:
-                case BOTTOM_TO_BOTTOM: {
+                case BOTTOM_TO_BOTTOM:
+                case BOTTOM_TO_BASELINE: {
                     mMarginBottom = value;
                 }
                 break;
@@ -773,12 +790,14 @@
                 }
                 break;
                 case TOP_TO_TOP:
-                case TOP_TO_BOTTOM: {
+                case TOP_TO_BOTTOM:
+                case TOP_TO_BASELINE: {
                     mMarginTopGone = value;
                 }
                 break;
                 case BOTTOM_TO_TOP:
-                case BOTTOM_TO_BOTTOM: {
+                case BOTTOM_TO_BOTTOM:
+                case BOTTOM_TO_BASELINE: {
                     mMarginBottomGone = value;
                 }
                 break;
@@ -835,8 +854,10 @@
             case CENTER_VERTICALLY:
             case TOP_TO_TOP:
             case TOP_TO_BOTTOM:
+            case TOP_TO_BASELINE:
             case BOTTOM_TO_TOP:
-            case BOTTOM_TO_BOTTOM: {
+            case BOTTOM_TO_BOTTOM:
+            case BOTTOM_TO_BASELINE: {
                 mVerticalBias = value;
             }
             break;
@@ -918,17 +939,21 @@
                 }
                 break;
                 case TOP_TO_TOP:
-                case TOP_TO_BOTTOM: {
+                case TOP_TO_BOTTOM:
+                case TOP_TO_BASELINE: {
                     mTopToTop = null;
                     mTopToBottom = null;
+                    mTopToBaseline = null;
                     mMarginTop = 0;
                     mMarginTopGone = 0;
                 }
                 break;
                 case BOTTOM_TO_TOP:
-                case BOTTOM_TO_BOTTOM: {
+                case BOTTOM_TO_BOTTOM:
+                case BOTTOM_TO_BASELINE: {
                     mBottomToTop = null;
                     mBottomToBottom = null;
+                    mBottomToBaseline = null;
                     mMarginBottom = 0;
                     mMarginBottomGone = 0;
                 }
@@ -1021,6 +1046,11 @@
                         ConstraintAnchor.Type.BOTTOM), mMarginTop, mMarginTopGone, false);
             }
             break;
+            case TOP_TO_BASELINE: {
+                widget.immediateConnect(ConstraintAnchor.Type.TOP, target,
+                        ConstraintAnchor.Type.BASELINE, mMarginTop, mMarginTopGone);
+            }
+            break;
             case BOTTOM_TO_TOP: {
                 widget.getAnchor(ConstraintAnchor.Type.BOTTOM).connect(target.getAnchor(
                         ConstraintAnchor.Type.TOP), mMarginBottom, mMarginBottomGone, false);
@@ -1031,6 +1061,11 @@
                         ConstraintAnchor.Type.BOTTOM), mMarginBottom, mMarginBottomGone, false);
             }
             break;
+            case BOTTOM_TO_BASELINE: {
+                widget.immediateConnect(ConstraintAnchor.Type.BOTTOM, target,
+                        ConstraintAnchor.Type.BASELINE, mMarginBottom, mMarginBottomGone);
+            }
+            break;
             case BASELINE_TO_BASELINE: {
                 widget.immediateConnect(ConstraintAnchor.Type.BASELINE, target,
                         ConstraintAnchor.Type.BASELINE, mMarginBaseline, mMarginBaselineGone);
@@ -1069,8 +1104,10 @@
         applyConnection(mConstraintWidget, mEndToEnd, State.Constraint.END_TO_END);
         applyConnection(mConstraintWidget, mTopToTop, State.Constraint.TOP_TO_TOP);
         applyConnection(mConstraintWidget, mTopToBottom, State.Constraint.TOP_TO_BOTTOM);
+        applyConnection(mConstraintWidget, mTopToBaseline, State.Constraint.TOP_TO_BASELINE);
         applyConnection(mConstraintWidget, mBottomToTop, State.Constraint.BOTTOM_TO_TOP);
         applyConnection(mConstraintWidget, mBottomToBottom, State.Constraint.BOTTOM_TO_BOTTOM);
+        applyConnection(mConstraintWidget, mBottomToBaseline, State.Constraint.BOTTOM_TO_BASELINE);
         applyConnection(mConstraintWidget, mBaselineToBaseline,
                 State.Constraint.BASELINE_TO_BASELINE);
         applyConnection(mConstraintWidget, mBaselineToTop, State.Constraint.BASELINE_TO_TOP);
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 c425c62..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;
@@ -1727,7 +1747,8 @@
             switch (constraintName) {
                 case "pathArc":
                     String val = obj.getString(constraintName);
-                    int ord = indexOf(val, "none", "startVertical", "startHorizontal", "flip");
+                    int ord = indexOf(val, "none", "startVertical", "startHorizontal", "flip",
+                            "below", "above");
                     if (ord == -1) {
                         System.err.println(obj.getLine() + " pathArc = '" + val + "'");
                         break;
@@ -1822,6 +1843,11 @@
                             break;
                         case "bottom":
                             reference.topToBottom(targetReference);
+                            break;
+                        case "baseline":
+                            state.baselineNeededFor(targetReference.getKey());
+                            reference.topToBaseline(targetReference);
+                            break;
                     }
                     break;
                 case "bottom":
@@ -1831,6 +1857,10 @@
                             break;
                         case "bottom":
                             reference.bottomToBottom(targetReference);
+                            break;
+                        case "baseline":
+                            state.baselineNeededFor(targetReference.getKey());
+                            reference.bottomToBaseline(targetReference);
                     }
                     break;
                 case "baseline":
@@ -1842,12 +1872,10 @@
                             break;
                         case "top":
                             state.baselineNeededFor(reference.getKey());
-                            state.baselineNeededFor(targetReference.getKey());
                             reference.baselineToTop(targetReference);
                             break;
                         case "bottom":
                             state.baselineNeededFor(reference.getKey());
-                            state.baselineNeededFor(targetReference.getKey());
                             reference.baselineToBottom(targetReference);
                             break;
                     }
@@ -1974,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) {
@@ -2041,4 +2069,4 @@
         }
         return null;
     }
-}
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/State.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/State.java
index d083aa7..3ea521d 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/State.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/State.java
@@ -67,8 +67,10 @@
         END_TO_END,
         TOP_TO_TOP,
         TOP_TO_BOTTOM,
+        TOP_TO_BASELINE,
         BOTTOM_TO_TOP,
         BOTTOM_TO_BOTTOM,
+        BOTTOM_TO_BASELINE,
         BASELINE_TO_BASELINE,
         BASELINE_TO_TOP,
         BASELINE_TO_BOTTOM,
@@ -186,6 +188,7 @@
     }
 
     public State() {
+        mParent.setKey(PARENT);
         mReferences.put(PARENT, mParent);
     }
 
@@ -238,7 +241,7 @@
      */
     public int convertDimension(Object value) {
         if (value instanceof Float) {
-            return (int) (((Float) value) + 0.5f);
+            return Math.round((Float) value);
         }
         if (value instanceof Integer) {
             return (Integer) value;
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/TransitionParser.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/TransitionParser.java
index cf3eba7..66e351a 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/TransitionParser.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/TransitionParser.java
@@ -76,6 +76,12 @@
                     break;
                 case "flip":
                     bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 3);
+                    break;
+                case "below":
+                    bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 4);
+                    break;
+                case "above":
+                    bundle.add(TypedValues.PositionType.TYPE_PATH_MOTION_ARC, 5);
             }
 
         }
@@ -235,7 +241,7 @@
 
             if (pathMotionArc != null) {
                 map(bundle, TypedValues.PositionType.TYPE_PATH_MOTION_ARC, pathMotionArc,
-                        "none", "startVertical", "startHorizontal", "flip");
+                        "none", "startVertical", "startHorizontal", "flip", "below", "above");
             }
 
             for (int j = 0; j < frames.size(); j++) {
diff --git a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/WidgetFrame.java b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/WidgetFrame.java
index 5406294..4adf478 100644
--- a/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/WidgetFrame.java
+++ b/constraintlayout/constraintlayout-core/src/main/java/androidx/constraintlayout/core/state/WidgetFrame.java
@@ -16,6 +16,7 @@
 
 package androidx.constraintlayout.core.state;
 
+import androidx.annotation.NonNull;
 import androidx.constraintlayout.core.motion.CustomAttribute;
 import androidx.constraintlayout.core.motion.CustomVariable;
 import androidx.constraintlayout.core.motion.utils.TypedBundle;
@@ -320,6 +321,13 @@
         return this;
     }
 
+    /**
+     * Return whether this WidgetFrame contains a custom property of the given name.
+     */
+    public boolean containsCustom(@NonNull String name) {
+        return mCustom.containsKey(name);
+    }
+
     // @TODO: add description
     public void addCustomColor(String name, int color) {
         setCustomAttribute(name, TypedValues.Custom.TYPE_COLOR, color);
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-core/src/test/java/androidx/constraintlayout/core/motion/MotionArcCurveTest.java b/constraintlayout/constraintlayout-core/src/test/java/androidx/constraintlayout/core/motion/MotionArcCurveTest.java
index e45efda..d5b2812 100644
--- a/constraintlayout/constraintlayout-core/src/test/java/androidx/constraintlayout/core/motion/MotionArcCurveTest.java
+++ b/constraintlayout/constraintlayout-core/src/test/java/androidx/constraintlayout/core/motion/MotionArcCurveTest.java
@@ -22,6 +22,8 @@
 
 import org.junit.Test;
 
+import java.util.Arrays;
+
 public class MotionArcCurveTest {
     @Test
     public void arcTest1() {
@@ -53,4 +55,142 @@
         assertEquals(1 - Math.sqrt(0.5), x, 0.001);
         assertEquals(Math.sqrt(0.5), y, 0.001);
     }
+
+    @Test
+    public void arcTest2() {
+        double[][] points = {
+                {0, 0}, {1, 1}, {2, 0}
+        };
+        double[] time = {
+                0, 5, 10
+        };
+        int[] mode = {
+                ArcCurveFit.ARC_BELOW,
+                ArcCurveFit.ARC_BELOW,
+
+        };
+        CurveFit spline = CurveFit.getArc(mode, time, points);
+        System.out.println("");
+        for (int i = 0; i < time.length; i++) {
+            assertEquals(points[i][0], spline.getPos(time[i], 0), 0.001);
+            assertEquals(points[i][1], spline.getPos(time[i], 1), 0.001);
+        }
+        assertEquals(0, spline.getSlope(time[0] + 0.01, 0), 0.001);
+        assertEquals(0, spline.getSlope(time[1] - 0.01, 1), 0.001);
+        assertEquals(0, spline.getSlope(time[1] + 0.01, 1), 0.001);
+        double dx = spline.getSlope((time[0] + time[1]) / 2, 0);
+        double dy = spline.getSlope((time[0] + time[1]) / 2, 1);
+        assertEquals(1, dx / dy, 0.001);
+        double x = spline.getPos((time[0] + time[1]) / 2, 0);
+        double y = spline.getPos((time[0] + time[1]) / 2, 1);
+        assertEquals(1 - Math.sqrt(0.5), x, 0.001);
+        assertEquals(Math.sqrt(0.5), y, 0.001);
+    }
+
+    @Test
+    public void arcTest3() {
+        double[][] points = {
+                {0, 0}, {1, 1}, {2, 0}
+        };
+        double[] time = {
+                0, 5, 10
+        };
+        int[] mode = {
+                ArcCurveFit.ARC_ABOVE,
+                ArcCurveFit.ARC_ABOVE,
+
+        };
+        CurveFit spline = CurveFit.getArc(mode, time, points);
+        System.out.println("");
+        for (int i = 0; i < time.length; i++) {
+            assertEquals(points[i][0], spline.getPos(time[i], 0), 0.001);
+            assertEquals(points[i][1], spline.getPos(time[i], 1), 0.001);
+        }
+        int count = 50;
+        float dt = (float) (time[time.length - 1] - time[0]) / count - 0.0001f;
+        float[] xp = new float[count];
+        float[] yp = new float[count];
+        for (int i = 0; i < xp.length; i++) {
+            double p = time[0] + i * dt;
+            xp[i] = (float) spline.getPos(p, 0);
+            yp[i] = (float) spline.getPos(p, 1);
+        }
+        String expect = ""
+                + "|*****                    *****| 0.0\n"
+                + "|     **                **     |\n"
+                + "|       **            **       |\n"
+                + "|        *            *        |\n"
+                + "|         *          *         |\n"
+                + "|          *        *          | 0.263\n"
+                + "|           *      **          |\n"
+                + "|            *    *            |\n"
+                + "|            *    *            |\n"
+                + "|             *  *             |\n"
+                + "|             *  *             | 0.526\n"
+                + "|                              |\n"
+                + "|             *  *             |\n"
+                + "|              **              |\n"
+                + "|              **              |\n"
+                + "|              **              | 0.789\n"
+                + "|              **              |\n"
+                + "|              **              |\n"
+                + "|                              |\n"
+                + "|              *               | 0.999\n"
+                + "0.0                        1.936\n";
+        assertEquals(expect, textDraw(30, 20, xp, yp, false));
+        assertEquals(0, spline.getSlope(time[0] + 0.0001, 1), 0.001);
+        assertEquals(0, spline.getSlope(time[1] - 0.01, 0), 0.001);
+        assertEquals(0, spline.getSlope(time[1] + 0.01, 0), 0.001);
+        double dx = spline.getSlope((time[0] + time[1]) / 2, 0);
+        double dy = spline.getSlope((time[0] + time[1]) / 2, 1);
+        assertEquals(1, dx / dy, 0.001);
+        double x = spline.getPos((time[0] + time[1]) / 2, 1);
+        double y = spline.getPos((time[0] + time[1]) / 2, 0);
+        assertEquals(1 - Math.sqrt(0.5), x, 0.001);
+        assertEquals(Math.sqrt(0.5), y, 0.001);
+    }
+
+
+    private static String textDraw(int dimx, int dimy, float[] x, float[] y, boolean flip) {
+        float minX = x[0], maxX = x[0], minY = y[0], maxY = y[0];
+        String ret = "";
+        for (int i = 0; i < x.length; i++) {
+            minX = Math.min(minX, x[i]);
+            maxX = Math.max(maxX, x[i]);
+            minY = Math.min(minY, y[i]);
+            maxY = Math.max(maxY, y[i]);
+        }
+        char[][] c = new char[dimy][dimx];
+        for (int i = 0; i < dimy; i++) {
+            Arrays.fill(c[i], ' ');
+        }
+        int dimx1 = dimx - 1;
+        int dimy1 = dimy - 1;
+        for (int j = 0; j < x.length; j++) {
+            int xp = (int) (dimx1 * (x[j] - minX) / (maxX - minX));
+            int yp = (int) (dimy1 * (y[j] - minY) / (maxY - minY));
+
+            c[flip ? dimy - yp - 1 : yp][xp] = '*';
+        }
+
+        for (int i = 0; i < c.length; i++) {
+            float v;
+            if (flip) {
+                v = (minY - maxY) * (i / (c.length - 1.0f)) + maxY;
+            } else {
+                v = (maxY - minY) * (i / (c.length - 1.0f)) + minY;
+            }
+            v = ((int) (v * 1000 + 0.5)) / 1000.f;
+            if (i % 5 == 0 || i == c.length - 1) {
+                ret += "|" + new String(c[i]) + "| " + v + "\n";
+            } else {
+                ret += "|" + new String(c[i]) + "|\n";
+            }
+        }
+        String minStr = Float.toString(((int) (minX * 1000 + 0.5)) / 1000.f);
+        String maxStr = Float.toString(((int) (maxX * 1000 + 0.5)) / 1000.f);
+        String s = minStr + new String(new char[dimx]).replace('\0', ' ');
+        s = s.substring(0, dimx - maxStr.length() + 2) + maxStr + "\n";
+        return ret + s;
+    }
 }
diff --git a/constraintlayout/constraintlayout/api/current.txt b/constraintlayout/constraintlayout/api/current.txt
index 5a48848..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!);
@@ -388,6 +402,7 @@
     field public static final String PERCENT_Y = "percentY";
     field public static final String SIZE_PERCENT = "sizePercent";
     field public static final String TRANSITION_EASING = "transitionEasing";
+    field public static final int TYPE_AXIS = 3; // 0x3
     field public static final int TYPE_CARTESIAN = 0; // 0x0
     field public static final int TYPE_PATH = 1; // 0x1
     field public static final int TYPE_SCREEN = 2; // 0x2
@@ -1106,7 +1121,7 @@
     method protected void setSelfDimensionBehaviour(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, int, int, int, int);
     method public void setState(int, int, int);
     field public static final int DESIGN_INFO_ID = 0; // 0x0
-    field public static final String VERSION = "ConstraintLayout-2.2.0-alpha03";
+    field public static final String VERSION = "ConstraintLayout-2.2.0-alpha04";
     field protected androidx.constraintlayout.widget.ConstraintLayoutStates! mConstraintLayoutSpec;
     field protected boolean mDirtyHierarchy;
     field protected androidx.constraintlayout.core.widgets.ConstraintWidgetContainer! mLayoutWidget;
diff --git a/constraintlayout/constraintlayout/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout/api/public_plus_experimental_current.txt
index 5a48848..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!);
@@ -388,6 +402,7 @@
     field public static final String PERCENT_Y = "percentY";
     field public static final String SIZE_PERCENT = "sizePercent";
     field public static final String TRANSITION_EASING = "transitionEasing";
+    field public static final int TYPE_AXIS = 3; // 0x3
     field public static final int TYPE_CARTESIAN = 0; // 0x0
     field public static final int TYPE_PATH = 1; // 0x1
     field public static final int TYPE_SCREEN = 2; // 0x2
@@ -1106,7 +1121,7 @@
     method protected void setSelfDimensionBehaviour(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, int, int, int, int);
     method public void setState(int, int, int);
     field public static final int DESIGN_INFO_ID = 0; // 0x0
-    field public static final String VERSION = "ConstraintLayout-2.2.0-alpha03";
+    field public static final String VERSION = "ConstraintLayout-2.2.0-alpha04";
     field protected androidx.constraintlayout.widget.ConstraintLayoutStates! mConstraintLayoutSpec;
     field protected boolean mDirtyHierarchy;
     field protected androidx.constraintlayout.core.widgets.ConstraintWidgetContainer! mLayoutWidget;
diff --git a/constraintlayout/constraintlayout/api/restricted_current.txt b/constraintlayout/constraintlayout/api/restricted_current.txt
index 5a48848..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!);
@@ -388,6 +402,7 @@
     field public static final String PERCENT_Y = "percentY";
     field public static final String SIZE_PERCENT = "sizePercent";
     field public static final String TRANSITION_EASING = "transitionEasing";
+    field public static final int TYPE_AXIS = 3; // 0x3
     field public static final int TYPE_CARTESIAN = 0; // 0x0
     field public static final int TYPE_PATH = 1; // 0x1
     field public static final int TYPE_SCREEN = 2; // 0x2
@@ -1106,7 +1121,7 @@
     method protected void setSelfDimensionBehaviour(androidx.constraintlayout.core.widgets.ConstraintWidgetContainer!, int, int, int, int);
     method public void setState(int, int, int);
     field public static final int DESIGN_INFO_ID = 0; // 0x0
-    field public static final String VERSION = "ConstraintLayout-2.2.0-alpha03";
+    field public static final String VERSION = "ConstraintLayout-2.2.0-alpha04";
     field protected androidx.constraintlayout.widget.ConstraintLayoutStates! mConstraintLayoutSpec;
     field protected boolean mDirtyHierarchy;
     field protected androidx.constraintlayout.core.widgets.ConstraintWidgetContainer! mLayoutWidget;
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/KeyPosition.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/KeyPosition.java
index ad5ea0c..252d04e 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/KeyPosition.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/KeyPosition.java
@@ -51,6 +51,7 @@
     float mPercentY = Float.NaN;
     float mAltPercentX = Float.NaN;
     float mAltPercentY = Float.NaN;
+    public static final int TYPE_AXIS = 3;
     public static final int TYPE_SCREEN = 2;
     public static final int TYPE_PATH = 1;
     public static final int TYPE_CARTESIAN = 0;
@@ -159,6 +160,9 @@
             case TYPE_SCREEN:
                 positionScreenAttributes(view, start, end, x, y, attribute, value);
                 return;
+            case TYPE_AXIS:
+                positionAxisAttributes(start, end, x, y, attribute, value);
+                return;
             case TYPE_CARTESIAN:
             default:
                 positionCartAttributes(start, end, x, y, attribute, value);
@@ -265,6 +269,44 @@
         }
     }
 
+    void positionAxisAttributes(RectF start,
+                                RectF end,
+                                float x,
+                                float y,
+                                String[] attribute,
+                                float[] value) {
+        float startCenterX = start.centerX();
+        float startCenterY = start.centerY();
+        float endCenterX = end.centerX();
+        float endCenterY = end.centerY();
+        if (startCenterX > endCenterX) {
+            float tmp = startCenterX;
+            startCenterX = endCenterX;
+            endCenterX = tmp;
+        }
+        if (startCenterY > endCenterY) {
+            float tmp = startCenterY;
+            startCenterY = endCenterY;
+            endCenterY = tmp;
+        }
+        float pathVectorX = endCenterX - startCenterX;
+        float pathVectorY = endCenterY - startCenterY;
+        if (attribute[0] != null) { // they are saying what to use
+            if (PERCENT_X.equals(attribute[0])) {
+                value[0] = (x - startCenterX) / pathVectorX;
+                value[1] = (y - startCenterY) / pathVectorY;
+            } else {
+                value[1] = (x - startCenterX) / pathVectorX;
+                value[0] = (y - startCenterY) / pathVectorY;
+            }
+        } else { // we will use what we want to
+            attribute[0] = PERCENT_X;
+            value[0] = (x - startCenterX) / pathVectorX;
+            attribute[1] = PERCENT_Y;
+            value[1] = (y - startCenterY) / pathVectorY;
+        }
+    }
+
     @Override
     public boolean intersects(int layoutWidth,
                               int layoutHeight,
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/motion/widget/MotionPaths.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/MotionPaths.java
index 92495e0..b695b5b 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/MotionPaths.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/motion/widget/MotionPaths.java
@@ -121,6 +121,63 @@
     }
 
     /**
+     * set up with Axis Relative 0,0 = top,left of bounding rectangle of start and end
+     *
+     * @param c
+     * @param startTimePoint
+     * @param endTimePoint
+     */
+    void initAxis(KeyPosition c, MotionPaths startTimePoint, MotionPaths endTimePoint) {
+        float position = c.mFramePosition / 100f;
+        MotionPaths point = this;
+        point.mTime = position;
+
+        mDrawPath = c.mDrawPath;
+        float scaleWidth = Float.isNaN(c.mPercentWidth) ? position : c.mPercentWidth;
+        float scaleHeight = Float.isNaN(c.mPercentHeight) ? position : c.mPercentHeight;
+        float scaleX = endTimePoint.mWidth - startTimePoint.mWidth;
+        float scaleY = endTimePoint.mHeight - startTimePoint.mHeight;
+
+        point.mPosition = point.mTime;
+
+        float path = position; // the position on the path
+
+        float startCenterX = startTimePoint.mX + startTimePoint.mWidth / 2;
+        float startCenterY = startTimePoint.mY + startTimePoint.mHeight / 2;
+        float endCenterX = endTimePoint.mX + endTimePoint.mWidth / 2;
+        float endCenterY = endTimePoint.mY + endTimePoint.mHeight / 2;
+        if (startCenterX > endCenterX) {
+            float tmp = startCenterX;
+            startCenterX = endCenterX;
+            endCenterX = tmp;
+        }
+        if (startCenterY > endCenterY) {
+            float tmp = startCenterY;
+            startCenterY = endCenterY;
+            endCenterY = tmp;
+        }
+        float pathVectorX = endCenterX - startCenterX;
+        float pathVectorY = endCenterY - startCenterY;
+        point.mX = (int) (startTimePoint.mX + pathVectorX * path - scaleX * scaleWidth / 2);
+        point.mY = (int) (startTimePoint.mY + pathVectorY * path - scaleY * scaleHeight / 2);
+        point.mWidth = (int) (startTimePoint.mWidth + scaleX * scaleWidth);
+        point.mHeight = (int) (startTimePoint.mHeight + scaleY * scaleHeight);
+
+        float dxdx = Float.isNaN(c.mPercentX) ? position : c.mPercentX;
+        float dydx = Float.isNaN(c.mAltPercentY) ? 0 : c.mAltPercentY;
+        float dydy = Float.isNaN(c.mPercentY) ? position : c.mPercentY;
+        float dxdy = Float.isNaN(c.mAltPercentX) ? 0 : c.mAltPercentX;
+        point.mMode = MotionPaths.CARTESIAN;
+        point.mX = (int) (startTimePoint.mX + pathVectorX * dxdx + pathVectorY * dxdy
+                - scaleX * scaleWidth / 2);
+        point.mY = (int) (startTimePoint.mY + pathVectorX * dydx + pathVectorY * dydy
+                - scaleY * scaleHeight / 2);
+
+        point.mKeyFrameEasing = Easing.getInterpolator(c.mTransitionEasing);
+        point.mPathMotionArc = c.mPathMotionArc;
+    }
+
+    /**
      * takes the new keyPosition
      *
      * @param c
@@ -143,6 +200,9 @@
             case KeyPosition.TYPE_PATH:
                 initPath(c, startTimePoint, endTimePoint);
                 return;
+            case KeyPosition.TYPE_AXIS:
+                initAxis(c, startTimePoint, endTimePoint);
+                return;
             default:
             case KeyPosition.TYPE_CARTESIAN:
                 initCartesian(c, startTimePoint, endTimePoint);
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 d1dae87..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,13 +537,12 @@
  * <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 {
     /**
      *
      */
-    public static final String VERSION = "ConstraintLayout-2.2.0-alpha03";
+    public static final String VERSION = "ConstraintLayout-2.2.0-alpha04";
     private static final String TAG = "ConstraintLayout";
 
     private static final boolean USE_CONSTRAINTS_HELPER = true;
diff --git a/constraintlayout/constraintlayout/src/main/res/values/attrs.xml b/constraintlayout/constraintlayout/src/main/res/values/attrs.xml
index c610941..aba4fb6 100644
--- a/constraintlayout/constraintlayout/src/main/res/values/attrs.xml
+++ b/constraintlayout/constraintlayout/src/main/res/values/attrs.xml
@@ -642,6 +642,8 @@
         <enum name="startVertical" value="1" />
         <enum name="startHorizontal" value="2" />
         <enum name="flip" value="3" />
+        <enum name="below" value="4" />
+        <enum name="above" value="5" />
     </attr>
 
     <attr name="polarRelativeTo" format="reference" />
@@ -814,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" />
@@ -1423,6 +1440,7 @@
             <enum name="deltaRelative" value="0" />
             <enum name="pathRelative" value="1" />
             <enum name="parentRelative" value="2" />
+            <enum name="axisRelative" value="3" />
         </attr>
 
         <!--  Percent distance from start to end along X axis (deltaRelative)
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..f90d936 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialManager.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialManager.kt
@@ -103,7 +103,7 @@
      * @throws UnsupportedOperationException Since the api is unimplemented
      */
     // TODO(helenqin): support failure flow.
-    suspend fun executeGetCredential(
+    suspend fun getCredential(
         request: GetCredentialRequest,
         activity: Activity,
     ): GetCredentialResponse = suspendCancellableCoroutine { continuation ->
@@ -123,7 +123,7 @@
             }
         }
 
-        executeGetCredentialAsync(
+        getCredentialAsync(
             request,
             activity,
             canceller,
@@ -144,7 +144,7 @@
      * @param activity the activity used to potentially launch any UI needed
      * @throws UnsupportedOperationException Since the api is unimplemented
      */
-    suspend fun executeCreateCredential(
+    suspend fun createCredential(
         request: CreateCredentialRequest,
         activity: Activity,
     ): CreateCredentialResponse = suspendCancellableCoroutine { continuation ->
@@ -164,7 +164,7 @@
             }
         }
 
-        executeCreateCredentialAsync(
+        createCredentialAsync(
             request,
             activity,
             canceller,
@@ -228,7 +228,7 @@
      * @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 +241,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
@@ -263,7 +263,7 @@
      * @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 +275,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
         }
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 3273ce5..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\'
@@ -706,6 +706,21 @@
 WARN: Missing @param tag for parameter `clipOp` of function androidx\.compose\.ui\.graphics/Canvas/clipRect/\#kotlin\.Float\#kotlin\.Float\#kotlin\.Float\#kotlin\.Float\#androidx\.compose\.ui\.graphics\.ClipOp/PointingToDeclaration/
 WARN: Missing @param tag for parameter `paint` of function androidx\.compose\.ui\.graphics/Canvas/drawArc/\#kotlin\.Float\#kotlin\.Float\#kotlin\.Float\#kotlin\.Float\#kotlin\.Float\#kotlin\.Float\#kotlin\.Boolean\#androidx\.compose\.ui\.graphics\.Paint/PointingToDeclaration/
 WARN: Missing @param tag for parameter `operation` of function androidx\.compose\.ui\.graphics/AndroidPath/op/\#androidx\.compose\.ui\.graphics\.Path\#androidx\.compose\.ui\.graphics\.Path\#androidx\.compose\.ui\.graphics\.PathOperation/PointingToDeclaration/
+WARN\: Multiple sources exist for RestrictTo\. Artifact ID metadata will not be displayed
+WARN\: Multiple sources exist for Scope\. Artifact ID metadata will not be displayed
+WARN\: Multiple sources exist for SparseArrayCompat\. Artifact ID metadata will not be displayed
+WARN\: Multiple sources exist for LongSparseArray\. Artifact ID metadata will not be displayed
+WARNING: link to @throws type IOException does not resolve\. Is it from a package that the containing file does not import\? Is docs inherited to an un\-documented override function, but the exception class is not in scope in the inheriting class\? The general fix for these is to fully qualify the exception name,  e\.g\.`@throws java\.io\.IOException under some conditions\. This was observed in Throws\(root=CustomDocTag\(children=\[P\(children=\[Text\(body=Unrecoverable IO exception when trying to access the underlying storage\., children=\[\], params=\{\}\)\], params=\{\}\)\], params=\{\}, name=MARKDOWN_FILE\), name=IOException, exceptionAddress=null\)\.`
+WARN: Multiple sources exist for PreferencesSerializer\. Artifact ID metadata will not be displayed
+WARN: Multiple sources exist for PreferenceDataStoreFactory\. Artifact ID metadata will not be displayed
+WARN: Multiple sources exist for DataStoreFactory\. Artifact ID metadata will not be displayed
+WARN: Multiple sources exist for ReplaceFileCorruptionHandler\. Artifact ID metadata will not be displayed
+WARNING\: link to \@throws type ArrayIndexOutOfBoundsException does not resolve\. Is it from a package that the containing file does not import\? Is docs inherited to an un\-documented override function\, but the exception class is not in scope in the inheriting class\? The general fix for these is to fully qualify the exception name\,  e\.g\.\`\@throws java\.io\.IOException under some conditions\. This was observed in Throws\(root\=CustomDocTag\(children\=\[P\(children\=\[Text\(body\=if \, children\=\[\]\, params\=\{\}\)\, DocumentationLink\(dri\=androidx\.collection\/CircularArray\/\/\/PointingToDeclaration\/\, children\=\[Text\(body\=CircularArray\, children\=\[\]\, params\=\{\}\)\]\, params\=\{href\=\[CircularArray\]\}\)\, Text\(body\= is empty\, children\=\[\]\, params\=\{\}\)\]\, params\=\{\}\)\]\, params\=\{\}\, name\=MARKDOWN_FILE\)\, name\=ArrayIndexOutOfBoundsException\, exceptionAddress\=null\)\.\`
+WARNING\: link to \@throws type ArrayIndexOutOfBoundsException does not resolve\. Is it from a package that the containing file does not import\? Is docs inherited to an un\-documented override function\, but the exception class is not in scope in the inheriting class\? The general fix for these is to fully qualify the exception name\,  e\.g\.\`\@throws java\.io\.IOException under some conditions\. This was observed in Throws\(root\=CustomDocTag\(children\=\[P\(children\=\[Text\(body\=if n \, children\=\[\]\, params\=\{\}\)\, Text\(body\=\<\, children\=\[\]\, params\=\{content\-type\=html\}\)\, Text\(body\= [0-9]+ or n \, children\=\[\]\, params\=\{\}\)\, Text\(body\=\>\, children\=\[\]\, params\=\{content\-type\=html\}\)\, Text\(body\=\= size\(\)\, children\=\[\]\, params\=\{\}\)\]\, params\=\{\}\)\]\, params\=\{\}\, name\=MARKDOWN_FILE\)\, name\=ArrayIndexOutOfBoundsException\, exceptionAddress\=null\)\.\`
+WARNING\: link to \@throws type ArrayIndexOutOfBoundsException does not resolve\. Is it from a package that the containing file does not import\? Is docs inherited to an un\-documented override function\, but the exception class is not in scope in the inheriting class\? The general fix for these is to fully qualify the exception name\,  e\.g\.\`\@throws java\.io\.IOException under some conditions\. This was observed in Throws\(root\=CustomDocTag\(children\=\[P\(children\=\[Text\(body\=if \, children\=\[\]\, params\=\{\}\)\, DocumentationLink\(dri\=androidx\.collection\/CircularArray\/\/\/PointingToDeclaration\/\, children\=\[Text\(body\=CircularArray\, children\=\[\]\, params\=\{\}\)\]\, params\=\{href\=\[CircularArray\]\}\)\, Text\(body\= is empty \(on jvm\)\, children\=\[\]\, params\=\{\}\)\]\, params\=\{\}\)\]\, params\=\{\}\, name\=MARKDOWN_FILE\)\, name\=ArrayIndexOutOfBoundsException\, exceptionAddress\=null\)\.\`
+WARNING\: link to \@throws type ArrayIndexOutOfBoundsException does not resolve\. Is it from a package that the containing file does not import\? Is docs inherited to an un\-documented override function\, but the exception class is not in scope in the inheriting class\? The general fix for these is to fully qualify the exception name\,  e\.g\.\`\@throws java\.io\.IOException under some conditions\. This was observed in Throws\(root\=CustomDocTag\(children\=\[P\(children\=\[Text\(body\=if \, children\=\[\]\, params\=\{\}\)\, DocumentationLink\(dri\=androidx\.collection\/CircularArray\/removeFromEnd\/\#kotlin\.Int\/PointingToCallableParameters\([0-9]+\)\/\, children\=\[Text\(body\=count\, children\=\[\]\, params\=\{\}\)\]\, params\=\{href\=\[count\]\}\)\, Text\(body\= is larger than \, children\=\[\]\, params\=\{\}\)\, DocumentationLink\(dri\=androidx\.collection\/CircularArray\/size\/\#\/PointingToDeclaration\/\, children\=\[Text\(body\=size\, children\=\[\]\, params\=\{\}\)\]\, params\=\{href\=\[size\]\}\)\]\, params\=\{\}\)\]\, params\=\{\}\, name\=MARKDOWN_FILE\)\, name\=ArrayIndexOutOfBoundsException\, exceptionAddress\=null\)\.\`
+WARNING\: link to \@throws type ArrayIndexOutOfBoundsException does not resolve\. Is it from a package that the containing file does not import\? Is docs inherited to an un\-documented override function\, but the exception class is not in scope in the inheriting class\? The general fix for these is to fully qualify the exception name\,  e\.g\.\`\@throws java\.io\.IOException under some conditions\. This was observed in Throws\(root\=CustomDocTag\(children\=\[P\(children\=\[Text\(body\=if \, children\=\[\]\, params\=\{\}\)\, DocumentationLink\(dri\=androidx\.collection\/CircularArray\/removeFromStart\/\#kotlin\.Int\/PointingToCallableParameters\([0-9]+\)\/\, children\=\[Text\(body\=count\, children\=\[\]\, params\=\{\}\)\]\, params\=\{href\=\[count\]\}\)\, Text\(body\= is larger than \, children\=\[\]\, params\=\{\}\)\, DocumentationLink\(dri\=androidx\.collection\/CircularArray\/size\/\#\/PointingToDeclaration\/\, children\=\[Text\(body\=size\, children\=\[\]\, params\=\{\}\)\]\, params\=\{href\=\[size\]\}\)\]\, params\=\{\}\)\]\, params\=\{\}\, name\=MARKDOWN_FILE\)\, name\=ArrayIndexOutOfBoundsException\, exceptionAddress\=null\)\.\`
+WARN\: Multiple sources exist for ArraySet\. Artifact ID metadata will not be displayed
 # Wire proto generation, task :generateDebugProtos
 Writing .* to \$OUT_DIR/.*/build/generated/source/wire
 # > Task :compose:ui:ui-tooling:processDebugAndroidTestManifest
diff --git a/development/checkstyle/.gitignore b/development/checkstyle/.gitignore
deleted file mode 100644
index d163863..0000000
--- a/development/checkstyle/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-build/
\ No newline at end of file
diff --git a/development/checkstyle/LICENSE b/development/checkstyle/LICENSE
deleted file mode 100644
index c1f5472..0000000
--- a/development/checkstyle/LICENSE
+++ /dev/null
@@ -1,502 +0,0 @@
-		  GNU LESSER GENERAL PUBLIC LICENSE
-		       Version 2.1, February 1999
-
- Copyright (C) 1991, 1999 Free Software Foundation, Inc.
-     59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-[This is the first released version of the Lesser GPL.  It also counts
- as the successor of the GNU Library Public License, version 2, hence
- the version number 2.1.]
-
-			    Preamble
-
-  The licenses for most software are designed to take away your
-freedom to share and change it.  By contrast, the GNU General Public
-Licenses are intended to guarantee your freedom to share and change
-free software--to make sure the software is free for all its users.
-
-  This license, the Lesser General Public License, applies to some
-specially designated software packages--typically libraries--of the
-Free Software Foundation and other authors who decide to use it.  You
-can use it too, but we suggest you first think carefully about whether
-this license or the ordinary General Public License is the better
-strategy to use in any particular case, based on the explanations below.
-
-  When we speak of free software, we are referring to freedom of use,
-not price.  Our General Public Licenses are designed to make sure that
-you have the freedom to distribute copies of free software (and charge
-for this service if you wish); that you receive source code or can get
-it if you want it; that you can change the software and use pieces of
-it in new free programs; and that you are informed that you can do
-these things.
-
-  To protect your rights, we need to make restrictions that forbid
-distributors to deny you these rights or to ask you to surrender these
-rights.  These restrictions translate to certain responsibilities for
-you if you distribute copies of the library or if you modify it.
-
-  For example, if you distribute copies of the library, whether gratis
-or for a fee, you must give the recipients all the rights that we gave
-you.  You must make sure that they, too, receive or can get the source
-code.  If you link other code with the library, you must provide
-complete object files to the recipients, so that they can relink them
-with the library after making changes to the library and recompiling
-it.  And you must show them these terms so they know their rights.
-
-  We protect your rights with a two-step method: (1) we copyright the
-library, and (2) we offer you this license, which gives you legal
-permission to copy, distribute and/or modify the library.
-
-  To protect each distributor, we want to make it very clear that
-there is no warranty for the free library.  Also, if the library is
-modified by someone else and passed on, the recipients should know
-that what they have is not the original version, so that the original
-author's reputation will not be affected by problems that might be
-introduced by others.
-
-  Finally, software patents pose a constant threat to the existence of
-any free program.  We wish to make sure that a company cannot
-effectively restrict the users of a free program by obtaining a
-restrictive license from a patent holder.  Therefore, we insist that
-any patent license obtained for a version of the library must be
-consistent with the full freedom of use specified in this license.
-
-  Most GNU software, including some libraries, is covered by the
-ordinary GNU General Public License.  This license, the GNU Lesser
-General Public License, applies to certain designated libraries, and
-is quite different from the ordinary General Public License.  We use
-this license for certain libraries in order to permit linking those
-libraries into non-free programs.
-
-  When a program is linked with a library, whether statically or using
-a shared library, the combination of the two is legally speaking a
-combined work, a derivative of the original library.  The ordinary
-General Public License therefore permits such linking only if the
-entire combination fits its criteria of freedom.  The Lesser General
-Public License permits more lax criteria for linking other code with
-the library.
-
-  We call this license the "Lesser" General Public License because it
-does Less to protect the user's freedom than the ordinary General
-Public License.  It also provides other free software developers Less
-of an advantage over competing non-free programs.  These disadvantages
-are the reason we use the ordinary General Public License for many
-libraries.  However, the Lesser license provides advantages in certain
-special circumstances.
-
-  For example, on rare occasions, there may be a special need to
-encourage the widest possible use of a certain library, so that it becomes
-a de-facto standard.  To achieve this, non-free programs must be
-allowed to use the library.  A more frequent case is that a free
-library does the same job as widely used non-free libraries.  In this
-case, there is little to gain by limiting the free library to free
-software only, so we use the Lesser General Public License.
-
-  In other cases, permission to use a particular library in non-free
-programs enables a greater number of people to use a large body of
-free software.  For example, permission to use the GNU C Library in
-non-free programs enables many more people to use the whole GNU
-operating system, as well as its variant, the GNU/Linux operating
-system.
-
-  Although the Lesser General Public License is Less protective of the
-users' freedom, it does ensure that the user of a program that is
-linked with the Library has the freedom and the wherewithal to run
-that program using a modified version of the Library.
-
-  The precise terms and conditions for copying, distribution and
-modification follow.  Pay close attention to the difference between a
-"work based on the library" and a "work that uses the library".  The
-former contains code derived from the library, whereas the latter must
-be combined with the library in order to run.
-
-		  GNU LESSER GENERAL PUBLIC LICENSE
-   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
-  0. This License Agreement applies to any software library or other
-program which contains a notice placed by the copyright holder or
-other authorized party saying it may be distributed under the terms of
-this Lesser General Public License (also called "this License").
-Each licensee is addressed as "you".
-
-  A "library" means a collection of software functions and/or data
-prepared so as to be conveniently linked with application programs
-(which use some of those functions and data) to form executables.
-
-  The "Library", below, refers to any such software library or work
-which has been distributed under these terms.  A "work based on the
-Library" means either the Library or any derivative work under
-copyright law: that is to say, a work containing the Library or a
-portion of it, either verbatim or with modifications and/or translated
-straightforwardly into another language.  (Hereinafter, translation is
-included without limitation in the term "modification".)
-
-  "Source code" for a work means the preferred form of the work for
-making modifications to it.  For a library, complete source code means
-all the source code for all modules it contains, plus any associated
-interface definition files, plus the scripts used to control compilation
-and installation of the library.
-
-  Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope.  The act of
-running a program using the Library is not restricted, and output from
-such a program is covered only if its contents constitute a work based
-on the Library (independent of the use of the Library in a tool for
-writing it).  Whether that is true depends on what the Library does
-and what the program that uses the Library does.
-
-  1. You may copy and distribute verbatim copies of the Library's
-complete source code as you receive it, in any medium, provided that
-you conspicuously and appropriately publish on each copy an
-appropriate copyright notice and disclaimer of warranty; keep intact
-all the notices that refer to this License and to the absence of any
-warranty; and distribute a copy of this License along with the
-Library.
-
-  You may charge a fee for the physical act of transferring a copy,
-and you may at your option offer warranty protection in exchange for a
-fee.
-
-  2. You may modify your copy or copies of the Library or any portion
-of it, thus forming a work based on the Library, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
-    a) The modified work must itself be a software library.
-
-    b) You must cause the files modified to carry prominent notices
-    stating that you changed the files and the date of any change.
-
-    c) You must cause the whole of the work to be licensed at no
-    charge to all third parties under the terms of this License.
-
-    d) If a facility in the modified Library refers to a function or a
-    table of data to be supplied by an application program that uses
-    the facility, other than as an argument passed when the facility
-    is invoked, then you must make a good faith effort to ensure that,
-    in the event an application does not supply such function or
-    table, the facility still operates, and performs whatever part of
-    its purpose remains meaningful.
-
-    (For example, a function in a library to compute square roots has
-    a purpose that is entirely well-defined independent of the
-    application.  Therefore, Subsection 2d requires that any
-    application-supplied function or table used by this function must
-    be optional: if the application does not supply it, the square
-    root function must still compute square roots.)
-
-These requirements apply to the modified work as a whole.  If
-identifiable sections of that work are not derived from the Library,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works.  But when you
-distribute the same sections as part of a whole which is a work based
-on the Library, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote
-it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Library.
-
-In addition, mere aggregation of another work not based on the Library
-with the Library (or with a work based on the Library) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
-  3. You may opt to apply the terms of the ordinary GNU General Public
-License instead of this License to a given copy of the Library.  To do
-this, you must alter all the notices that refer to this License, so
-that they refer to the ordinary GNU General Public License, version 2,
-instead of to this License.  (If a newer version than version 2 of the
-ordinary GNU General Public License has appeared, then you can specify
-that version instead if you wish.)  Do not make any other change in
-these notices.
-
-  Once this change is made in a given copy, it is irreversible for
-that copy, so the ordinary GNU General Public License applies to all
-subsequent copies and derivative works made from that copy.
-
-  This option is useful when you wish to copy part of the code of
-the Library into a program that is not a library.
-
-  4. You may copy and distribute the Library (or a portion or
-derivative of it, under Section 2) in object code or executable form
-under the terms of Sections 1 and 2 above provided that you accompany
-it with the complete corresponding machine-readable source code, which
-must be distributed under the terms of Sections 1 and 2 above on a
-medium customarily used for software interchange.
-
-  If distribution of object code is made by offering access to copy
-from a designated place, then offering equivalent access to copy the
-source code from the same place satisfies the requirement to
-distribute the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
-  5. A program that contains no derivative of any portion of the
-Library, but is designed to work with the Library by being compiled or
-linked with it, is called a "work that uses the Library".  Such a
-work, in isolation, is not a derivative work of the Library, and
-therefore falls outside the scope of this License.
-
-  However, linking a "work that uses the Library" with the Library
-creates an executable that is a derivative of the Library (because it
-contains portions of the Library), rather than a "work that uses the
-library".  The executable is therefore covered by this License.
-Section 6 states terms for distribution of such executables.
-
-  When a "work that uses the Library" uses material from a header file
-that is part of the Library, the object code for the work may be a
-derivative work of the Library even though the source code is not.
-Whether this is true is especially significant if the work can be
-linked without the Library, or if the work is itself a library.  The
-threshold for this to be true is not precisely defined by law.
-
-  If such an object file uses only numerical parameters, data
-structure layouts and accessors, and small macros and small inline
-functions (ten lines or less in length), then the use of the object
-file is unrestricted, regardless of whether it is legally a derivative
-work.  (Executables containing this object code plus portions of the
-Library will still fall under Section 6.)
-
-  Otherwise, if the work is a derivative of the Library, you may
-distribute the object code for the work under the terms of Section 6.
-Any executables containing that work also fall under Section 6,
-whether or not they are linked directly with the Library itself.
-
-  6. As an exception to the Sections above, you may also combine or
-link a "work that uses the Library" with the Library to produce a
-work containing portions of the Library, and distribute that work
-under terms of your choice, provided that the terms permit
-modification of the work for the customer's own use and reverse
-engineering for debugging such modifications.
-
-  You must give prominent notice with each copy of the work that the
-Library is used in it and that the Library and its use are covered by
-this License.  You must supply a copy of this License.  If the work
-during execution displays copyright notices, you must include the
-copyright notice for the Library among them, as well as a reference
-directing the user to the copy of this License.  Also, you must do one
-of these things:
-
-    a) Accompany the work with the complete corresponding
-    machine-readable source code for the Library including whatever
-    changes were used in the work (which must be distributed under
-    Sections 1 and 2 above); and, if the work is an executable linked
-    with the Library, with the complete machine-readable "work that
-    uses the Library", as object code and/or source code, so that the
-    user can modify the Library and then relink to produce a modified
-    executable containing the modified Library.  (It is understood
-    that the user who changes the contents of definitions files in the
-    Library will not necessarily be able to recompile the application
-    to use the modified definitions.)
-
-    b) Use a suitable shared library mechanism for linking with the
-    Library.  A suitable mechanism is one that (1) uses at run time a
-    copy of the library already present on the user's computer system,
-    rather than copying library functions into the executable, and (2)
-    will operate properly with a modified version of the library, if
-    the user installs one, as long as the modified version is
-    interface-compatible with the version that the work was made with.
-
-    c) Accompany the work with a written offer, valid for at
-    least three years, to give the same user the materials
-    specified in Subsection 6a, above, for a charge no more
-    than the cost of performing this distribution.
-
-    d) If distribution of the work is made by offering access to copy
-    from a designated place, offer equivalent access to copy the above
-    specified materials from the same place.
-
-    e) Verify that the user has already received a copy of these
-    materials or that you have already sent this user a copy.
-
-  For an executable, the required form of the "work that uses the
-Library" must include any data and utility programs needed for
-reproducing the executable from it.  However, as a special exception,
-the materials to be distributed need not include anything that is
-normally distributed (in either source or binary form) with the major
-components (compiler, kernel, and so on) of the operating system on
-which the executable runs, unless that component itself accompanies
-the executable.
-
-  It may happen that this requirement contradicts the license
-restrictions of other proprietary libraries that do not normally
-accompany the operating system.  Such a contradiction means you cannot
-use both them and the Library together in an executable that you
-distribute.
-
-  7. You may place library facilities that are a work based on the
-Library side-by-side in a single library together with other library
-facilities not covered by this License, and distribute such a combined
-library, provided that the separate distribution of the work based on
-the Library and of the other library facilities is otherwise
-permitted, and provided that you do these two things:
-
-    a) Accompany the combined library with a copy of the same work
-    based on the Library, uncombined with any other library
-    facilities.  This must be distributed under the terms of the
-    Sections above.
-
-    b) Give prominent notice with the combined library of the fact
-    that part of it is a work based on the Library, and explaining
-    where to find the accompanying uncombined form of the same work.
-
-  8. You may not copy, modify, sublicense, link with, or distribute
-the Library except as expressly provided under this License.  Any
-attempt otherwise to copy, modify, sublicense, link with, or
-distribute the Library is void, and will automatically terminate your
-rights under this License.  However, parties who have received copies,
-or rights, from you under this License will not have their licenses
-terminated so long as such parties remain in full compliance.
-
-  9. You are not required to accept this License, since you have not
-signed it.  However, nothing else grants you permission to modify or
-distribute the Library or its derivative works.  These actions are
-prohibited by law if you do not accept this License.  Therefore, by
-modifying or distributing the Library (or any work based on the
-Library), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Library or works based on it.
-
-  10. Each time you redistribute the Library (or any work based on the
-Library), the recipient automatically receives a license from the
-original licensor to copy, distribute, link with or modify the Library
-subject to these terms and conditions.  You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties with
-this License.
-
-  11. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Library at all.  For example, if a patent
-license would not permit royalty-free redistribution of the Library by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Library.
-
-If any portion of this section is held invalid or unenforceable under any
-particular circumstance, the balance of the section is intended to apply,
-and the section as a whole is intended to apply in other circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system which is
-implemented by public license practices.  Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
-  12. If the distribution and/or use of the Library is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Library under this License may add
-an explicit geographical distribution limitation excluding those countries,
-so that distribution is permitted only in or among countries not thus
-excluded.  In such case, this License incorporates the limitation as if
-written in the body of this License.
-
-  13. The Free Software Foundation may publish revised and/or new
-versions of the Lesser General Public License from time to time.
-Such new versions will be similar in spirit to the present version,
-but may differ in detail to address new problems or concerns.
-
-Each version is given a distinguishing version number.  If the Library
-specifies a version number of this License which applies to it and
-"any later version", you have the option of following the terms and
-conditions either of that version or of any later version published by
-the Free Software Foundation.  If the Library does not specify a
-license version number, you may choose any version ever published by
-the Free Software Foundation.
-
-  14. If you wish to incorporate parts of the Library into other free
-programs whose distribution conditions are incompatible with these,
-write to the author to ask for permission.  For software which is
-copyrighted by the Free Software Foundation, write to the Free
-Software Foundation; we sometimes make exceptions for this.  Our
-decision will be guided by the two goals of preserving the free status
-of all derivatives of our free software and of promoting the sharing
-and reuse of software generally.
-
-			    NO WARRANTY
-
-  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
-WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
-EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
-OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
-KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
-LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
-THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
-  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
-WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
-AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
-FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
-CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
-LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
-RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
-FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
-SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
-DAMAGES.
-
-		     END OF TERMS AND CONDITIONS
-
-           How to Apply These Terms to Your New Libraries
-
-  If you develop a new library, and you want it to be of the greatest
-possible use to the public, we recommend making it free software that
-everyone can redistribute and change.  You can do so by permitting
-redistribution under these terms (or, alternatively, under the terms of the
-ordinary General Public License).
-
-  To apply these terms, attach the following notices to the library.  It is
-safest to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least the
-"copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the library's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This library is free software; you can redistribute it and/or
-    modify it under the terms of the GNU Lesser General Public
-    License as published by the Free Software Foundation; either
-    version 2.1 of the License, or (at your option) any later version.
-
-    This library is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-    Lesser General Public License for more details.
-
-    You should have received a copy of the GNU Lesser General Public
-    License along with this library; if not, write to the Free Software
-    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-
-Also add information on how to contact you by electronic and paper mail.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the library, if
-necessary.  Here is a sample; alter the names:
-
-  Yoyodyne, Inc., hereby disclaims all copyright interest in the
-  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
-
-  <signature of Ty Coon>, 1 April 1990
-  Ty Coon, President of Vice
-
-That's all there is to it!
diff --git a/development/checkstyle/README b/development/checkstyle/README
deleted file mode 100644
index 8f9bf38..0000000
--- a/development/checkstyle/README
+++ /dev/null
@@ -1,16 +0,0 @@
-Description:
-Checkstyle is used by developers to validate Java code style before running
-repo upload. This project implements Checkstyle checks specific to the Android
-support library.
-
-Projects used:
-* Name: Checkstyle
-  Description: Checkstyle is a development tool to help programmers write Java
-               code that adheres to a coding standard.
-  URL: http://checkstyle.sourceforge.net/
-  Version: 6.12.1
-  License: LGPL 2.1
-  License File: LICENSE
-  Local Modifications:
-  - The only source file used here is MissingDeprecatedCheck, which was adapted
-    to create MissingRestrictToCheck.
diff --git a/development/checkstyle/build.gradle b/development/checkstyle/build.gradle
deleted file mode 100644
index 1121887..0000000
--- a/development/checkstyle/build.gradle
+++ /dev/null
@@ -1,20 +0,0 @@
-apply plugin: "java"
-
-compileJava {
-    sourceCompatibility = JavaVersion.VERSION_1_7
-    targetCompatibility = JavaVersion.VERSION_1_7
-}
-
-dependencies {
-    compile files("../../../../prebuilts/checkstyle/checkstyle.jar")
-}
-
-sourceSets {
-    main.java.srcDir "src"
-}
-
-jar {
-    from sourceSets.main.output
-    baseName = "com.android.support.checkstyle"
-    destinationDir = new File("prebuilt")
-}
diff --git a/development/checkstyle/config/support-lib.xml b/development/checkstyle/config/support-lib.xml
deleted file mode 100644
index 49ecc5c..0000000
--- a/development/checkstyle/config/support-lib.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-  ~ Copyright (C) 2016 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
--->
-<!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN" "http://www.puppycrawl.com/dtds/configuration_1_3.dtd" [
-    <!ENTITY defaultCopyrightCheck SYSTEM "../../../../../prebuilts/checkstyle/default-copyright-check.xml">
-    <!ENTITY defaultJavadocChecks SYSTEM "../../../../../prebuilts/checkstyle/default-javadoc-checks.xml">
-    <!ENTITY defaultTreewalkerChecks SYSTEM "../../../../../prebuilts/checkstyle/default-treewalker-checks.xml">
-    <!ENTITY defaultModuleChecks SYSTEM "../../../../../prebuilts/checkstyle/default-module-checks.xml">
-    ]>
-
-<module name="Checker">
-    &defaultModuleChecks;
-    &defaultCopyrightCheck;
-    <module name="TreeWalker">
-        &defaultJavadocChecks;
-        &defaultTreewalkerChecks;
-
-        <module name="RedundantModifierCheck" />
-
-        <!-- Custom support library check for @RestrictTo / @hide. -->
-        <module name="com.android.support.checkstyle.MismatchedAnnotationCheck">
-            <property name="severity" value="error" />
-            <property name="tag" value="hide" />
-            <property name="annotation" value="androidx.annotation.RestrictTo" />
-            <property name="messageKey" value="annotation.missing.hide" />
-            <message key="annotation.missing.hide" value="Must include both @RestrictTo annotation and @hide Javadoc tag."/>
-        </module>
-    </module>
-</module>
diff --git a/development/checkstyle/gradle/wrapper/gradle-wrapper.jar b/development/checkstyle/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index 13372ae..0000000
--- a/development/checkstyle/gradle/wrapper/gradle-wrapper.jar
+++ /dev/null
Binary files differ
diff --git a/development/checkstyle/gradle/wrapper/gradle-wrapper.properties b/development/checkstyle/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index cb1b272..0000000
--- a/development/checkstyle/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-#Tue Aug 16 10:43:36 PDT 2016
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=../../../../../../tools/external/gradle/gradle-4.1-bin.zip
diff --git a/development/checkstyle/gradlew b/development/checkstyle/gradlew
deleted file mode 100755
index 9d82f78..0000000
--- a/development/checkstyle/gradlew
+++ /dev/null
@@ -1,160 +0,0 @@
-#!/usr/bin/env bash
-
-##############################################################################
-##
-##  Gradle start up script for UN*X
-##
-##############################################################################
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
-
-warn ( ) {
-    echo "$*"
-}
-
-die ( ) {
-    echo
-    echo "$*"
-    echo
-    exit 1
-}
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-case "`uname`" in
-  CYGWIN* )
-    cygwin=true
-    ;;
-  Darwin* )
-    darwin=true
-    ;;
-  MINGW* )
-    msys=true
-    ;;
-esac
-
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
-    ls=`ls -ld "$PRG"`
-    link=`expr "$ls" : '.*-> \(.*\)$'`
-    if expr "$link" : '/.*' > /dev/null; then
-        PRG="$link"
-    else
-        PRG=`dirname "$PRG"`"/$link"
-    fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
-    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
-        # IBM's JDK on AIX uses strange locations for the executables
-        JAVACMD="$JAVA_HOME/jre/sh/java"
-    else
-        JAVACMD="$JAVA_HOME/bin/java"
-    fi
-    if [ ! -x "$JAVACMD" ] ; then
-        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-    fi
-else
-    JAVACMD="java"
-    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-fi
-
-# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
-    MAX_FD_LIMIT=`ulimit -H -n`
-    if [ $? -eq 0 ] ; then
-        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
-            MAX_FD="$MAX_FD_LIMIT"
-        fi
-        ulimit -n $MAX_FD
-        if [ $? -ne 0 ] ; then
-            warn "Could not set maximum file descriptor limit: $MAX_FD"
-        fi
-    else
-        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
-    fi
-fi
-
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
-    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
-
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
-    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
-    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-    JAVACMD=`cygpath --unix "$JAVACMD"`
-
-    # We build the pattern for arguments to be converted via cygpath
-    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
-    SEP=""
-    for dir in $ROOTDIRSRAW ; do
-        ROOTDIRS="$ROOTDIRS$SEP$dir"
-        SEP="|"
-    done
-    OURCYGPATTERN="(^($ROOTDIRS))"
-    # Add a user-defined pattern to the cygpath arguments
-    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
-        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
-    fi
-    # Now convert the arguments - kludge to limit ourselves to /bin/sh
-    i=0
-    for arg in "$@" ; do
-        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
-        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
-
-        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
-            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
-        else
-            eval `echo args$i`="\"$arg\""
-        fi
-        i=$((i+1))
-    done
-    case $i in
-        (0) set -- ;;
-        (1) set -- "$args0" ;;
-        (2) set -- "$args0" "$args1" ;;
-        (3) set -- "$args0" "$args1" "$args2" ;;
-        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
-        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
-        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
-        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
-        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
-        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
-    esac
-fi
-
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
-    JVM_OPTS=("$@")
-}
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
-
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/development/checkstyle/prebuilt/com.android.support.checkstyle.jar b/development/checkstyle/prebuilt/com.android.support.checkstyle.jar
deleted file mode 100644
index c59a474..0000000
--- a/development/checkstyle/prebuilt/com.android.support.checkstyle.jar
+++ /dev/null
Binary files differ
diff --git a/development/checkstyle/src/com/android/support/checkstyle/MismatchedAnnotationCheck.java b/development/checkstyle/src/com/android/support/checkstyle/MismatchedAnnotationCheck.java
deleted file mode 100644
index a1a60dff..0000000
--- a/development/checkstyle/src/com/android/support/checkstyle/MismatchedAnnotationCheck.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * checkstyle: Checks Java source code for adherence to a set of rules.
- * Copyright (C) 2001-2016 the original author or authors.
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- */
-
-package com.android.support.checkstyle;
-
-import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
-import com.puppycrawl.tools.checkstyle.api.DetailAST;
-import com.puppycrawl.tools.checkstyle.api.TextBlock;
-import com.puppycrawl.tools.checkstyle.api.TokenTypes;
-import com.puppycrawl.tools.checkstyle.utils.AnnotationUtility;
-import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
-
-import java.util.regex.Pattern;
-
-/**
- * This class is used to verify that both an annotation and javadoc tag are
- * present when either one is present.
- * <p>
- * Typically, both ways of flagging APIs serve their own purposes. Annotations
- * are used for compilers and development tools, while javadoc tags are used
- * for documentation.
- * <p>
- * In some cases, the presence of an annotations implies the presence of a
- * javadoc tag (or vice versa). For example, in the case of the
- * {@literal @}Deprecated annotation, the {@literal @}deprecated tag should
- * also be present. In the case of the {@literal @}RestrictTo tag, the
- * {@literal @}hide tag should also be present.
- * <p>
- * To configure this check, do the following:
- * <pre>
- *     &lt;module name="MismatchedAnnotationCheck"&gt;
- *       &lt;property name="tag" value="hide" /&gt;
- *       &lt;property name="annotation" value="android.support.annotation.RestrictTo" /&gt;
- *       &lt;property name="messageKey" value="annotation.missing.hide" /&gt;
- *       &lt;message key="annotation.missing.hide"
- *                   value="Must include both {@literal @}RestrictTo annotation
- *                          and {@literal @}hide Javadoc tag." /&gt;
- *     &lt;/module&gt;
- * </pre>
- */
-@SuppressWarnings("unused")
-public final class MismatchedAnnotationCheck extends AbstractCheck {
-
-    /** Key for the warning message text by check properties. */
-    private static final String MSG_KEY_JAVADOC_DUPLICATE_TAG = "javadoc.duplicateTag";
-
-    /** Key for the warning message text by check properties. */
-    private static final String MSG_KEY_JAVADOC_MISSING = "javadoc.missing";
-
-    /** Javadoc tag. */
-    private String mTag;
-
-    /** Pattern for matching javadoc tag. */
-    private Pattern mMatchTag;
-
-    /** Simple annotation name. */
-    private String mAnnotationSimpleName;
-
-    /** Fully-qualified annotation name. */
-    private String mAnnotation;
-
-    /** Key for the warning message text specified by check properties. */
-    private String mMessageKey;
-
-    @Override
-    public int[] getDefaultTokens() {
-        return getAcceptableTokens();
-    }
-
-    /**
-     * Sets javadoc tag.
-     *
-     * @param tag javadoc tag to check
-     */
-    @SuppressWarnings("unused")
-    public void setTag(String tag) {
-        mTag = tag;
-
-        // Tag may either have a description or be on a line by itself.
-        mMatchTag = CommonUtils.createPattern("@" + tag + "(?:\\s|$)");
-    }
-
-    /**
-     * Sets annotation tag.
-     *
-     * @param annotation annotation to check
-     */
-    @SuppressWarnings("unused")
-    public void setAnnotation(String annotation) {
-        mAnnotation = annotation;
-
-        // Extract the simple class name.
-        final int lastDollar = annotation.lastIndexOf('$');
-        final int lastSep = lastDollar >= 0 ? lastDollar : annotation.lastIndexOf('.');
-        mAnnotationSimpleName = annotation.substring(lastSep + 1);
-    }
-
-
-    /**
-     * Sets annotation tag.
-     *
-     * @param messageKey key to use for failed check message
-     */
-    @SuppressWarnings("unused")
-    public void setMessageKey(String messageKey) {
-        mMessageKey = messageKey;
-    }
-
-    @Override
-    public int[] getAcceptableTokens() {
-        return new int[] {
-                TokenTypes.INTERFACE_DEF,
-                TokenTypes.CLASS_DEF,
-                TokenTypes.ANNOTATION_DEF,
-                TokenTypes.ENUM_DEF,
-                TokenTypes.METHOD_DEF,
-                TokenTypes.CTOR_DEF,
-                TokenTypes.VARIABLE_DEF,
-                TokenTypes.ENUM_CONSTANT_DEF,
-                TokenTypes.ANNOTATION_FIELD_DEF,
-        };
-    }
-
-    @Override
-    public int[] getRequiredTokens() {
-        return getAcceptableTokens();
-    }
-
-    @Override
-    public void visitToken(final DetailAST ast) {
-        final boolean containsAnnotation =
-                AnnotationUtility.containsAnnotation(ast, mAnnotationSimpleName)
-                        || AnnotationUtility.containsAnnotation(ast, mAnnotation);
-        final boolean containsJavadocTag = containsJavadocTag(ast);
-        if (containsAnnotation ^ containsJavadocTag) {
-            log(ast.getLineNo(), mMessageKey);
-        }
-    }
-
-    /**
-     * Checks to see if the text block contains the tag.
-     *
-     * @param ast the AST being visited
-     * @return true if contains the tag
-     */
-    private boolean containsJavadocTag(final DetailAST ast) {
-        final TextBlock javadoc = getFileContents().getJavadocBefore(ast.getLineNo());
-        if (javadoc == null) {
-            return false;
-        }
-
-        int currentLine = javadoc.getStartLineNo();
-        boolean found = false;
-
-        final String[] lines = javadoc.getText();
-        for (String line : lines) {
-            if (mMatchTag.matcher(line).find()) {
-                if (found) {
-                    log(currentLine, MSG_KEY_JAVADOC_DUPLICATE_TAG, mTag);
-                }
-                found = true;
-            }
-            currentLine++;
-        }
-
-        return found;
-    }
-}
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 9ebbebc..498ff9f 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -16,9 +16,11 @@
     docs(project(":ads:ads-identifier-common"))
     docs(project(":ads:ads-identifier-provider"))
     docs(project(":ads:ads-identifier-testing"))
-    docs(project(":annotation:annotation"))
+    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"))
@@ -56,7 +58,7 @@
     docs(project(":car:app:app-projected"))
     docs(project(":car:app:app-testing"))
     docs(project(":cardview:cardview"))
-    docs(project(":collection:collection"))
+    kmpDocs(project(":collection:collection"))
     docs(project(":collection:collection-ktx"))
     docs(project(":compose:animation:animation"))
     docs(project(":compose:animation:animation-core"))
@@ -134,9 +136,10 @@
     docs(project(":customview:customview"))
     docs(project(":customview:customview-poolingcontainer"))
     docs(project(":datastore:datastore"))
-    docs(project(":datastore:datastore-core"))
+    kmpDocs(project(":datastore:datastore-core"))
+    kmpDocs(project(":datastore:datastore-core-okio"))
     docs(project(":datastore:datastore-preferences"))
-    docs(project(":datastore:datastore-preferences-core"))
+    kmpDocs(project(":datastore:datastore-preferences-core"))
     docs(project(":datastore:datastore-preferences-proto"))
     docs(project(":datastore:datastore-preferences-rxjava2"))
     docs(project(":datastore:datastore-preferences-rxjava3"))
@@ -248,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/emoji2/emoji2-bundled/api/res-1.3.0-beta01.txt b/emoji2/emoji2-bundled/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/emoji2/emoji2-bundled/api/res-1.3.0-beta01.txt
diff --git a/emoji2/emoji2-bundled/api/res-1.3.0-beta02.txt b/emoji2/emoji2-bundled/api/res-1.3.0-beta02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/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 141611d..45e6dbd 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
+++ b/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
@@ -15,12 +15,12 @@
   limitations under the License.
   -->
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools">
+<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/Theme.AppCompat.DayNight">
+            android:theme="@style/MyPinkTheme">
             <!-- Handle Google app icon launch. -->
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
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
new file mode 100644
index 0000000..32ff475
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/samples/src/main/res/values/styles.xml
@@ -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.
+  -->
+
+<resources>
+
+    <style name="MyPinkTheme" parent="Theme.AppCompat.DayNight" >
+        <!-- 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">#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>
+
+</resources>
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 96d02de..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
@@ -40,6 +39,7 @@
 import androidx.test.espresso.matcher.ViewMatchers.withId
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import org.hamcrest.Description
@@ -96,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
@@ -121,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
         )
@@ -188,6 +188,7 @@
         assertSelectedHeaderIndex(4)
     }
 
+    @FlakyTest(bugId = 265006871)
     @Test(expected = UnsupportedOperationException::class)
     fun testAddView_throwsException() {
         activityTestRule.scenario.onActivity {
@@ -227,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/EmojiPickerBodyAdapter.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
index b18d7a4..21a9b10 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
@@ -50,7 +50,7 @@
                 R.layout.empty_category_text_view, parent
             ) {
                 minimumHeight =
-                    emojiCellHeight ?: (parent.measuredHeight / emojiGridRows).toInt()
+                    emojiCellHeight ?: (getEmojiCellTotalHeight(parent) / (emojiGridRows)).toInt()
                         .also { emojiCellHeight = it }
             }
 
@@ -60,7 +60,7 @@
                     emojiCellWidth ?: (getParentWidth(parent) / emojiGridColumns).also {
                         emojiCellWidth = it
                     },
-                    emojiCellHeight ?: (parent.measuredHeight / emojiGridRows).toInt()
+                    emojiCellHeight ?: (getEmojiCellTotalHeight(parent) / (emojiGridRows)).toInt()
                         .also { emojiCellHeight = it },
                     layoutInflater,
                     stickyVariantProvider,
@@ -114,6 +114,13 @@
         return parent.measuredWidth - parent.paddingLeft - parent.paddingRight
     }
 
+    private fun getEmojiCellTotalHeight(parent: ViewGroup) =
+        parent.measuredHeight - context.resources.getDimensionPixelSize(
+            R.dimen.emoji_picker_category_name_height
+        ) * 2 - context.resources.getDimensionPixelSize(
+            R.dimen.emoji_picker_category_name_padding_top
+        )
+
     private fun createSimpleHolder(
         @LayoutRes layoutId: Int,
         parent: ViewGroup,
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/drawable/variant_availability_indicator.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
index e27fe2e..a471ae6 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
@@ -19,7 +19,7 @@
     android:height="24dp"
     android:viewportWidth="24.0"
     android:viewportHeight="24.0"
-    android:tint="?attr/colorControlNormal">
+    android:tint="?attr/colorButtonNormal">
     <path
         android:fillColor="@android:color/white"
         android:pathData="M2,22h20V2L2,22z"/>
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 2ed8ede..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
@@ -15,22 +15,21 @@
   limitations under the License.
   -->
 
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:importantForAccessibility="no">
     <androidx.appcompat.widget.AppCompatTextView
         android:id="@+id/category_name"
         android:layout_width="wrap_content"
-        android:layout_height="24dp"
+        android:layout_height="@dimen/emoji_picker_category_name_height"
         android:layout_alignParentStart="true"
         android:layout_alignParentTop="true"
-        android:paddingTop="4dp"
+        android:paddingTop="@dimen/emoji_picker_category_name_padding_top"
         android:paddingLeft="7dp"
         android:paddingRight="7dp"
         android:gravity="center_vertical|start"
         android:letterSpacing="0.1"
         android:importantForAccessibility="yes"
-        android:textColor="?android:attr/textColorPrimary"
-        android:textSize="12dp" />
-</RelativeLayout>
+        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-emojipicker/src/main/res/values/dimens.xml b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
index af3fdd2..7c2cd47 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
@@ -32,4 +32,6 @@
     <dimen name="emoji_picker_popup_view_elevation">8dp</dimen>
     <dimen name="emoji_picker_popup_view_holder_corner_radius">30dp</dimen>
     <dimen name="emoji_picker_skin_tone_circle_radius">6dp</dimen>
+    <dimen name="emoji_picker_category_name_height">24dp</dimen>
+    <dimen name="emoji_picker_category_name_padding_top">4dp</dimen>
 </resources>
\ No newline at end of file
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/emoji2/emoji2-views-helper/api/res-1.3.0-beta01.txt b/emoji2/emoji2-views-helper/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/emoji2/emoji2-views-helper/api/res-1.3.0-beta01.txt
diff --git a/emoji2/emoji2-views-helper/api/res-1.3.0-beta02.txt b/emoji2/emoji2-views-helper/api/res-1.3.0-beta02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/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/emoji2/emoji2/api/res-1.3.0-beta01.txt b/emoji2/emoji2/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/emoji2/emoji2/api/res-1.3.0-beta01.txt
diff --git a/emoji2/emoji2/api/res-1.3.0-beta02.txt b/emoji2/emoji2/api/res-1.3.0-beta02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/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/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/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 bc53366..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
@@ -35,8 +35,10 @@
 import android.widget.TextView
 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.graphics.Color
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.DpSize
@@ -98,6 +100,8 @@
 import kotlin.test.assertIs
 import kotlin.test.assertNotNull
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.collectIndexed
 import kotlinx.coroutines.flow.take
@@ -134,7 +138,6 @@
         TestGlanceAppWidget.sizeMode = SizeMode.Single
     }
 
-    @Ignore // b/261993523
     @Test
     fun createSimpleAppWidget() {
         TestGlanceAppWidget.uiDefinition = {
@@ -187,7 +190,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @FlakyTest(bugId = 249803914)
     @Test
     fun createResponsiveAppWidget() {
@@ -226,7 +228,7 @@
         }
     }
 
-    @Ignore // b/261993523
+    @Ignore("b/266588723")
     @Test
     fun createTextWithFillMaxDimensions() {
         TestGlanceAppWidget.uiDefinition = {
@@ -253,7 +255,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun createTextViewWithMixedDimensions() {
         TestGlanceAppWidget.uiDefinition = {
@@ -267,7 +268,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun createBoxWithExactDimensions() {
         TestGlanceAppWidget.uiDefinition = {
@@ -284,7 +284,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun createBoxWithMixedDimensions() {
         TestGlanceAppWidget.uiDefinition = {
@@ -302,7 +301,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun createColumnWithMixedDimensions() {
         TestGlanceAppWidget.uiDefinition = {
@@ -326,7 +324,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun createRowWithMixedDimensions() {
         TestGlanceAppWidget.uiDefinition = {
@@ -350,7 +347,7 @@
         }
     }
 
-    @Ignore // b/261993523
+    @Ignore("b/265078768")
     @Test
     fun createRowWithTwoTexts() {
         TestGlanceAppWidget.uiDefinition = {
@@ -376,7 +373,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun createColumnWithTwoTexts() {
         TestGlanceAppWidget.uiDefinition = {
@@ -402,7 +398,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun createColumnWithTwoTexts2() {
         TestGlanceAppWidget.uiDefinition = {
@@ -428,7 +423,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun createButton() {
         TestGlanceAppWidget.uiDefinition = {
@@ -455,7 +449,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun createImage() {
         TestGlanceAppWidget.uiDefinition = {
@@ -471,7 +464,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun drawableBackground() {
         TestGlanceAppWidget.uiDefinition = {
@@ -511,7 +503,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun bitmapBackground() {
         TestGlanceAppWidget.uiDefinition = {
@@ -538,7 +529,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun removeAppWidget() {
         TestGlanceAppWidget.uiDefinition = {
@@ -580,7 +570,6 @@
             .isFalse()
     }
 
-    @Ignore // b/261993523
     @Test
     fun updateAll() = runTest {
         TestGlanceAppWidget.uiDefinition = {
@@ -594,7 +583,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun updateIf() = runTest {
         val didRun = AtomicBoolean(false)
@@ -646,7 +634,6 @@
         assertThat(didRun.get()).isFalse()
     }
 
-    @Ignore // b/261993523
     @Test
     fun viewState() {
         TestGlanceAppWidget.uiDefinition = {
@@ -672,7 +659,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun actionCallback() {
         TestGlanceAppWidget.uiDefinition = {
@@ -714,7 +700,6 @@
         assertThat(CallbackTest.received.get()).containsExactly(1, 2)
     }
 
-    @Ignore // b/261993523
     @Test
     fun multipleActionCallback() {
         TestGlanceAppWidget.uiDefinition = {
@@ -746,7 +731,6 @@
         assertThat(CallbackTest.received.get()).containsExactly(2)
     }
 
-    @Ignore // b/261993523
     @Test
     fun wrapAroundFillMaxSize() {
         TestGlanceAppWidget.uiDefinition = {
@@ -802,8 +786,7 @@
                     checked = true,
                     onCheckedChange = actionRunCallback<CompoundButtonActionTest>(
                         actionParametersOf(CompoundButtonActionTest.key to switch)
-                    ),
-                    text = switch
+                    ), text = switch
                 )
             }
         }
@@ -864,7 +847,6 @@
         // if no crash, we're good
     }
 
-    @Ignore // b/261993523
     @Test
     fun radioActionCallback() {
         TestGlanceAppWidget.uiDefinition = {
@@ -889,7 +871,6 @@
         assertThat(CallbackTest.received.get()).containsExactly(2)
     }
 
-    @Ignore // b/261993523
     @Test
     fun lambdaActionCallback() = runTest {
         if (!useSessionManager) return@runTest
@@ -918,7 +899,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @FlakyTest(bugId = 259938473)
     @Test
     fun unsetActionCallback() = runTest {
@@ -937,7 +917,6 @@
         }
 
         mHostRule.startHost()
-
         mHostRule.onHostView { root ->
             val view =
                 checkNotNull(
@@ -945,7 +924,6 @@
                 )
             assertThat(view.hasOnClickListeners()).isTrue()
         }
-
         updateAppWidgetState(context, AppWidgetId(mHostRule.appWidgetId)) {
             it[testBoolKey] = false
         }
@@ -962,7 +940,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun unsetCompoundButtonActionCallback() = runTest {
         TestGlanceAppWidget.uiDefinition = {
@@ -1008,7 +985,6 @@
         assertThat(CompoundButtonActionTest.received.get()).isEmpty()
     }
 
-    @Ignore // b/261993523
     @SdkSuppress(minSdkVersion = 31)
     @Test
     fun compoundButtonsOnlyHaveOneAction() {
@@ -1036,7 +1012,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun elementsWithActionsHaveRipples() {
         TestGlanceAppWidget.uiDefinition = {
@@ -1059,7 +1034,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
     fun elementsWithNoActionsDontHaveRipples() {
         TestGlanceAppWidget.uiDefinition = {
@@ -1073,7 +1047,6 @@
         }
     }
 
-    @Ignore // b/261993523
     @SdkSuppress(minSdkVersion = 31)
     @Test
     fun compoundButtonsDoNotHaveRipples() {
@@ -1092,17 +1065,20 @@
         }
     }
 
-    @Ignore // b/261993523
     @Test
-    fun cancellingContentCoroutineMakesContentLeaveComposition() = runBlocking {
-        if (!useSessionManager) return@runBlocking
-
+        fun cancellingContentCoroutineCausesContentToLeaveComposition() = runBlocking {
+            if (!useSessionManager) return@runBlocking
         val currentEffectState = MutableStateFlow(EffectState.Initial)
-        TestGlanceAppWidget.uiDefinition = {
-            DisposableEffect(true) {
-                currentEffectState.tryEmit(EffectState.Started)
-                onDispose {
-                    currentEffectState.tryEmit(EffectState.Disposed)
+        var contentJob: Job? = null
+        TestGlanceAppWidget.onProvideGlance = {
+            coroutineScope {
+                contentJob = launch {
+                    provideContent {
+                        DisposableEffect(true) {
+                            currentEffectState.tryEmit(EffectState.Started)
+                            onDispose { currentEffectState.tryEmit(EffectState.Disposed) }
+                        }
+                    }
                 }
             }
         }
@@ -1112,7 +1088,7 @@
                 0 -> assertThat(state).isEqualTo(EffectState.Initial)
                 1 -> {
                     assertThat(state).isEqualTo(EffectState.Started)
-                    assertNotNull(TestGlanceAppWidget.contentCoroutine.get()).cancel()
+                    assertNotNull(contentJob).cancel()
                 }
                 2 -> assertThat(state).isEqualTo(EffectState.Disposed)
             }
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt
index c383082..feed04a 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt
@@ -35,7 +35,6 @@
 import androidx.glance.layout.Alignment
 import androidx.glance.layout.padding
 import androidx.glance.text.Text
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
@@ -69,7 +68,6 @@
         }
     }
 
-    @FlakyTest(bugId = 206481702)
     @Test
     fun item_withoutItemIds_createsNonStableList() {
         TestGlanceAppWidget.uiDefinition = {
@@ -148,7 +146,7 @@
             assertThat(adapter.getItemId(0)).isEqualTo(0L)
             assertThat(adapter.getItemId(1)).isEqualTo(2L)
             assertThat(adapter.getItemId(2)).isEqualTo(4L)
-            assertThat(adapter.getItemId(3)).isEqualTo(ReservedItemIdRangeEnd)
+            assertThat(adapter.getItemId(3)).isEqualTo(ReservedItemIdRangeEnd - 3)
         }
     }
 
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
index d2832ae..fb9ff6c 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
@@ -48,6 +48,17 @@
         uiDefinition()
     }
 
+    override suspend fun provideGlance(
+        context: Context,
+        id: GlanceId
+    ) {
+        onProvideGlance?.invoke(this)
+        onProvideGlance = null
+        provideContent(uiDefinition)
+    }
+
+    var onProvideGlance: (suspend TestGlanceAppWidget.() -> Unit)? = null
+
     private var onDeleteBlock: ((GlanceId) -> Unit)? = null
 
     fun setOnDeleteBlock(block: (GlanceId) -> Unit) {
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 40ceb00..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
@@ -24,12 +24,19 @@
 import androidx.annotation.VisibleForTesting
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 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
 import androidx.glance.GlanceComposable
+import androidx.glance.GlanceId
 import androidx.glance.LocalContext
 import androidx.glance.LocalGlanceId
 import androidx.glance.LocalState
@@ -38,9 +45,14 @@
 import androidx.glance.state.ConfigManager
 import androidx.glance.state.GlanceState
 import androidx.glance.state.PreferencesGlanceStateDefinition
-import java.util.concurrent.CancellationException
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
 
 /**
  * A session that composes UI for a single app widget.
@@ -78,43 +90,44 @@
 
     override fun createRootEmittable() = RemoteViewsRoot(MaxComposeTreeDepth)
 
-    override suspend fun provideGlance(
-        context: Context,
-    ): Flow<@Composable @GlanceComposable () -> Unit> {
-        val manager = context.appWidgetManager
-        val minSize = appWidgetMinSize(
-            context.resources.displayMetrics,
-            manager,
-            id.appWidgetId
-        )
-        options.value = initialOptions ?: manager.getAppWidgetOptions(id.appWidgetId)!!
-        glanceState.value =
-            configManager.getValue(context, PreferencesGlanceStateDefinition, key)
-        return widget.runGlance(context, id).map {
-                content: (@Composable @GlanceComposable () -> Unit)? ->
-            {
-                CompositionLocalProvider(
-                    LocalContext provides context,
-                    LocalGlanceId provides id,
-                    LocalAppWidgetOptions provides options.value,
-                    LocalState provides glanceState.value,
-                ) {
-                    if (content != null) {
-                        ForEachSize(widget.sizeMode, minSize, content)
-                    } else {
-                        IgnoreResult()
-                    }
-                }
+    override fun provideGlance(context: Context): @Composable @GlanceComposable () -> Unit = {
+        CompositionLocalProvider(
+            LocalContext provides context,
+            LocalGlanceId provides id,
+            LocalAppWidgetOptions provides options.value,
+            LocalState provides glanceState.value,
+        ) {
+            val manager = remember { context.appWidgetManager }
+            val minSize = remember {
+                appWidgetMinSize(
+                    context.resources.displayMetrics,
+                    manager,
+                    id.appWidgetId
+                )
             }
+            val configIsReady by produceState(false) {
+                options.value = initialOptions ?: manager.getAppWidgetOptions(id.appWidgetId)
+                glanceState.value =
+                    configManager.getValue(context, PreferencesGlanceStateDefinition, key)
+                value = true
+            }
+            remember { widget.runGlance(context, id) }
+                .collectAsState(null)
+                .takeIf { configIsReady }
+                ?.value?.let { ForEachSize(widget.sizeMode, minSize, it) }
+                ?: IgnoreResult()
+            // The following line ensures that when glanceState is updated, it increases the
+            // Recomposer.changeCount and triggers processEmittableTree.
+            SideEffect { glanceState.value }
         }
     }
 
     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 {
@@ -148,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) {
@@ -165,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(
@@ -195,7 +212,7 @@
         sendEvent(RunLambda(key))
     }
 
-    // Action types that this session supports.
+    // Event types that this session supports.
     @VisibleForTesting
     internal object UpdateGlanceState
     @VisibleForTesting
@@ -207,7 +224,44 @@
         get() = this.getSystemService(Context.APPWIDGET_SERVICE) as AppWidgetManager
 }
 
+internal fun interface ContentReceiver : CoroutineContext.Element {
+    /**
+     * Provide [content] to the Glance session, suspending until the session is
+     * shut down.
+     *
+     * If this function is called concurrently with itself, the previous call will throw
+     * [CancellationException] and the new content will replace it.
+     */
+    suspend fun provideContent(
+        content: @Composable @GlanceComposable () -> Unit
+    ): Nothing
+
+    override val key: CoroutineContext.Key<*> get() = Key
+
+    companion object Key : CoroutineContext.Key<ContentReceiver>
+}
+
+internal fun GlanceAppWidget.runGlance(
+    context: Context,
+    id: GlanceId,
+): Flow<(@GlanceComposable @Composable () -> Unit)?> = channelFlow {
+    val contentCoroutine: AtomicReference<CancellableContinuation<Nothing>?> =
+        AtomicReference(null)
+    val receiver = ContentReceiver { content ->
+        suspendCancellableCoroutine {
+            it.invokeOnCancellation { trySend(null) }
+            contentCoroutine.getAndSet(it)?.cancel()
+            trySend(content)
+        }
+    }
+    withContext(receiver) { provideGlance(context, id) }
+}
+
+internal val Context.appWidgetManager: AppWidgetManager
+    get() = this.getSystemService(Context.APPWIDGET_SERVICE) as AppWidgetManager
+
 internal fun createUniqueRemoteUiName(appWidgetId: Int) = "appWidget-$appWidgetId"
+
 internal fun AppWidgetId.toSessionKey() = createUniqueRemoteUiName(appWidgetId)
 
 /**
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt
index c94a102..51d1774 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ApplyModifiers.kt
@@ -45,6 +45,8 @@
 import androidx.glance.layout.HeightModifier
 import androidx.glance.layout.PaddingModifier
 import androidx.glance.layout.WidthModifier
+import androidx.glance.semantics.SemanticsModifier
+import androidx.glance.semantics.SemanticsProperties
 import androidx.glance.unit.Dimension
 import androidx.glance.unit.FixedColorProvider
 import androidx.glance.unit.ResourceColorProvider
@@ -64,6 +66,7 @@
     var actionModifier: ActionModifier? = null
     var enabled: EnabledModifier? = null
     var clipToOutline: ClipToOutlineModifier? = null
+    var semanticsModifier: SemanticsModifier? = null
     modifiers.foldIn(Unit) { _, modifier ->
         when (modifier) {
             is ActionModifier -> {
@@ -100,6 +103,7 @@
             }
             is ClipToOutlineModifier -> clipToOutline = modifier
             is EnabledModifier -> enabled = modifier
+            is SemanticsModifier -> semanticsModifier = modifier
             else -> {
                 Log.w(GlanceAppWidgetTag, "Unknown modifier '$modifier', nothing done.")
             }
@@ -127,6 +131,13 @@
     enabled?.let {
         rv.setBoolean(viewDef.mainViewId, "setEnabled", it.enabled)
     }
+    semanticsModifier?.let { semantics ->
+        val contentDescription: List<String>? =
+            semantics.configuration.getOrNull(SemanticsProperties.ContentDescription)
+        if (contentDescription != null) {
+            rv.setContentDescription(viewDef.mainViewId, contentDescription.joinToString())
+        }
+    }
     rv.setViewVisibility(viewDef.mainViewId, visibility.toViewVisibility())
 }
 
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
index 881d186..0dd2fd7 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -44,8 +44,7 @@
 import androidx.glance.state.GlanceState
 import androidx.glance.state.GlanceStateDefinition
 import androidx.glance.state.PreferencesGlanceStateDefinition
-import java.util.concurrent.atomic.AtomicReference
-import kotlinx.coroutines.CancellableContinuation
+import kotlin.coroutines.coroutineContext
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -53,11 +52,7 @@
 import kotlinx.coroutines.async
 import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.suspendCancellableCoroutine
 import kotlinx.coroutines.withContext
 
 /**
@@ -74,10 +69,6 @@
     @LayoutRes
     internal val errorUiLayout: Int = R.layout.glance_error_layout,
 ) {
-    internal val contentFlow = MutableStateFlow<(@Composable @GlanceComposable () -> Unit)?>(null)
-    internal val contentCoroutine: AtomicReference<CancellableContinuation<Nothing>?> =
-        AtomicReference(null)
-
     /**
      * Override this function to provide the Glance Composable.
      *
@@ -588,26 +579,7 @@
 suspend fun GlanceAppWidget.provideContent(
     content: @Composable @GlanceComposable () -> Unit
 ): Nothing {
-    suspendCancellableCoroutine<Nothing> {
-        it.invokeOnCancellation {
-            contentFlow.tryEmit(null)
-        }
-        contentCoroutine.getAndSet(it)?.cancel()
-        contentFlow.tryEmit(content)
-    }
-}
-
-/**
- * Returns a cold [kotlinx.coroutines.flow.Flow] that, upon collection, runs
- * [GlanceAppWidget.provideGlance] in a separate coroutine and provides any generated content to
- * the collectors of this flow.
- */
-internal fun GlanceAppWidget.runGlance(
-    context: Context,
-    id: GlanceId
-): Flow<(@Composable @GlanceComposable () -> Unit)?> = flow {
-    coroutineScope {
-        launch { provideGlance(context, id) }
-        contentFlow.collect { emit(it) }
-    }
+    coroutineContext[ContentReceiver]?.provideContent(content)
+        ?: error("provideContent requires a ContentReceiver and should only be called from " +
+            "GlanceAppWidget.provideGlance")
 }
\ No newline at end of file
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/NormalizeCompositionTree.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt
index 6f7a7cc..5336388 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt
@@ -27,6 +27,7 @@
 import androidx.glance.ImageProvider
 import androidx.glance.action.ActionModifier
 import androidx.glance.action.LambdaAction
+import androidx.glance.appwidget.action.CompoundButtonAction
 import androidx.glance.appwidget.lazy.EmittableLazyListItem
 import androidx.glance.background
 import androidx.glance.extractModifier
@@ -136,10 +137,9 @@
     children.foldIndexed(
         mutableMapOf<String, MutableList<LambdaAction>>()
     ) { index, actions, child ->
-        val (actionMod, modifiers) = child.modifier.extractModifier<ActionModifier>()
-        if (actionMod != null && actionMod.action is LambdaAction &&
-            child !is EmittableSizeBox && child !is EmittableLazyListItem) {
-            val action = actionMod.action as LambdaAction
+        val (action: LambdaAction?, modifiers: GlanceModifier) =
+            child.modifier.extractLambdaAction()
+        if (action != null && child !is EmittableSizeBox && child !is EmittableLazyListItem) {
             val newKey = action.key + "+$index"
             val newAction = LambdaAction(newKey, action.block)
             actions.getOrPut(newKey) { mutableListOf() }.add(newAction)
@@ -153,6 +153,17 @@
         actions
     }
 
+private fun GlanceModifier.extractLambdaAction(): Pair<LambdaAction?, GlanceModifier> =
+    extractModifier<ActionModifier>().let { (actionModifier, modifiers) ->
+        val action = actionModifier?.action
+        when {
+            action is LambdaAction -> action to modifiers
+            action is CompoundButtonAction && action.innerAction is LambdaAction ->
+                action.innerAction to modifiers
+            else -> null to modifiers
+        }
+    }
+
 private fun normalizeLazyListItem(view: EmittableLazyListItem) {
     if (view.children.size == 1 && view.alignment == Alignment.CenterStart) return
     val box = EmittableBox()
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/lazy/LazyList.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt
index d68d7e7..a9286e32 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyList.kt
@@ -58,7 +58,6 @@
     alignment: Alignment,
     content: LazyListScope.() -> Unit
 ): @Composable () -> Unit {
-    var nextImplicitItemId = ReservedItemIdRangeEnd
     val itemList = mutableListOf<Pair<Long?, @Composable LazyItemScope.() -> Unit>>()
     val listScopeImpl = object : LazyListScope {
         override fun item(itemId: Long, content: @Composable LazyItemScope.() -> Unit) {
@@ -83,8 +82,9 @@
     }
     listScopeImpl.apply(content)
     return {
-        itemList.forEach { (itemId, composable) ->
-            val id = itemId.takeIf { it != LazyListScope.UnspecifiedItemId } ?: nextImplicitItemId--
+        itemList.forEachIndexed { index, (itemId, composable) ->
+            val id = itemId.takeIf { it != LazyListScope.UnspecifiedItemId }
+                ?: (ReservedItemIdRangeEnd - index)
             check(id != LazyListScope.UnspecifiedItemId) { "Implicit list item ids exhausted." }
             LazyListItem(id, alignment) {
                 object : LazyItemScope { }.apply { composable() }
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
index cf37b7e..79ace6f 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/lazy/LazyVerticalGrid.kt
@@ -61,7 +61,6 @@
     alignment: Alignment,
     content: LazyVerticalGridScope.() -> Unit
 ): @Composable () -> Unit {
-    var nextImplicitItemId = ReservedItemIdRangeEnd
     val itemList = mutableListOf<Pair<Long?, @Composable LazyItemScope.() -> Unit>>()
     val listScopeImpl = object : LazyVerticalGridScope {
         override fun item(itemId: Long, content: @Composable LazyItemScope.() -> Unit) {
@@ -87,9 +86,9 @@
     }
     listScopeImpl.apply(content)
     return {
-        itemList.forEach { (itemId, composable) ->
-            val id = itemId.takeIf {
-              it != LazyVerticalGridScope.UnspecifiedItemId } ?: nextImplicitItemId--
+        itemList.forEachIndexed { index, (itemId, composable) ->
+            val id = itemId.takeIf { it != LazyVerticalGridScope.UnspecifiedItemId }
+                ?: (ReservedItemIdRangeEnd - index)
             check(id != LazyVerticalGridScope.UnspecifiedItemId) {
                 "Implicit list item ids exhausted."
             }
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 46c96e8..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,
@@ -53,7 +63,6 @@
         }
     }
     val viewDef = insertView(translationContext, selector, element.modifier)
-    setContentDescription(viewDef.mainViewId, element.contentDescription)
     when (val provider = element.provider) {
         is AndroidResourceImageProvider -> setImageViewResource(
             viewDef.mainViewId,
@@ -65,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
@@ -77,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.")
@@ -91,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 020f1b8..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
@@ -21,6 +21,7 @@
 import android.content.Context
 import android.widget.TextView
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Recomposer
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import androidx.glance.Emittable
@@ -45,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
@@ -54,7 +56,7 @@
 class AppWidgetSessionTest {
 
     private val id = AppWidgetId(123)
-    private val widget = SampleGlanceAppWidget {}
+    private val widget = TestWidget {}
     private val context = ApplicationProvider.getApplicationContext<Context>()
     private val defaultOptions =
         optionsBundleOf(listOf(DpSize(100.dp, 50.dp), DpSize(50.dp, 100.dp)))
@@ -76,14 +78,18 @@
 
     @Test
     fun provideGlanceRunsGlance() = runTest {
-        session.provideGlance(context).first()
+        runTestingComposition(session.provideGlance(context))
+        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.
-        val initialContent = session.provideGlance(context).first()
-        val root = runTestingComposition(initialContent)
+        val root = runCompositionUntil(
+            { state, _ -> state == Recomposer.State.Idle },
+            session.provideGlance(context)
+        )
         assertThat(root.shouldIgnoreResult()).isTrue()
     }
 
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt
index c449dcf..d0984d0 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt
@@ -40,6 +40,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.assertIs
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.collectIndexed
 import kotlinx.coroutines.flow.take
@@ -69,7 +70,7 @@
 
     @Test
     fun createEmptyUi() = fakeCoroutineScope.runTest {
-        val composer = SampleGlanceAppWidget { }
+        val composer = TestWidget { }
 
         val rv = composer.composeForSize(
             context,
@@ -87,7 +88,7 @@
 
     @Test
     fun createUiWithSize() = fakeCoroutineScope.runTest {
-        val composer = SampleGlanceAppWidget {
+        val composer = TestWidget {
             val size = LocalSize.current
             Text("${size.width} x ${size.height}")
         }
@@ -108,7 +109,7 @@
 
     @Test
     fun createUiFromOptionBundle() = fakeCoroutineScope.runTest {
-        val composer = SampleGlanceAppWidget {
+        val composer = TestWidget {
             val options = LocalAppWidgetOptions.current
 
             Text(options.getString("StringKey", "<NOT FOUND>"))
@@ -132,7 +133,7 @@
 
     @Test
     fun createUiFromGlanceId() = fakeCoroutineScope.runTest {
-        val composer = SampleGlanceAppWidget {
+        val composer = TestWidget {
             val glanceId = LocalGlanceId.current
 
             Text(glanceId.toString())
@@ -155,7 +156,7 @@
 
     @Test
     fun createUiWithUniqueMode() = fakeCoroutineScope.runTest {
-        val composer = SampleGlanceAppWidget {
+        val composer = TestWidget {
             val size = LocalSize.current
             Text("${size.width} x ${size.height}")
         }
@@ -188,7 +189,7 @@
     @Config(sdk = [30])
     @Test
     fun createUiWithExactModePreS() = fakeCoroutineScope.runTest {
-        val composer = SampleGlanceAppWidget(SizeMode.Exact) {
+        val composer = TestWidget(SizeMode.Exact) {
             val size = LocalSize.current
             Text("${size.width} x ${size.height}")
         }
@@ -222,7 +223,7 @@
             DpSize(100.dp, 70.dp),
             DpSize(120.dp, 100.dp),
         )
-        val composer = SampleGlanceAppWidget(SizeMode.Responsive(sizes)) {
+        val composer = TestWidget(SizeMode.Responsive(sizes)) {
             val size = LocalSize.current
             Text("${size.width} x ${size.height}")
         }
@@ -253,7 +254,7 @@
     @Test
     fun createUiWithExactMode_noSizeFallsBackToUnique() {
         runBlocking {
-            val composer = SampleGlanceAppWidget(SizeMode.Exact) {
+            val composer = TestWidget(SizeMode.Exact) {
                 val size = LocalSize.current
                 Text("${size.width} x ${size.height}")
             }
@@ -295,7 +296,7 @@
             DpSize(100.dp, 70.dp),
             DpSize(120.dp, 100.dp),
         )
-        val composer = SampleGlanceAppWidget(SizeMode.Responsive(sizes)) {
+        val composer = TestWidget(SizeMode.Responsive(sizes)) {
             val size = LocalSize.current
             Text("${size.width} x ${size.height}")
         }
@@ -458,22 +459,22 @@
     fun cancellingProvideContentEmitsNullContent() = runBlocking {
         val widget = object : GlanceAppWidget() {
             override suspend fun provideGlance(context: Context, id: GlanceId) {
-                val provideContentJob = launch { provideContent { Content() } }
-                delay(100)
-                provideContentJob.cancel()
+                coroutineScope {
+                    val provideContentJob = launch { provideContent { Text("") } }
+                    delay(100)
+                    provideContentJob.cancel()
+                }
             }
             override val sessionManager = GlanceSessionManager
             @Composable
             override fun Content() { }
         }
-        widget.runGlance(context, AppWidgetId(0)).take(3).collectIndexed { index, content ->
+        widget.runGlance(context, AppWidgetId(0)).take(2).collectIndexed { index, content ->
             when (index) {
-                // Initial state of content is null
-                0 -> assertThat(content).isNull()
-                // Content is non-null when provideContent is called
-                1 -> assertThat(content).isNotNull()
+                // Initial content
+                0 -> assertThat(content).isNotNull()
                 // Content is null again when provideContent is cancelled
-                2 -> assertThat(content).isNull()
+                1 -> assertThat(content).isNull()
                 else -> throw Error("Invalid index $index")
             }
         }
@@ -490,16 +491,6 @@
         config.orientation = orientation
         return context.createConfigurationContext(config)
     }
-
-    private class SampleGlanceAppWidget(
-        override val sizeMode: SizeMode = SizeMode.Single,
-        val ui: @Composable () -> Unit,
-    ) : GlanceAppWidget() {
-        @Composable
-        override fun Content() {
-            ui()
-        }
-    }
 }
 
 internal fun optionsBundleOf(sizes: List<DpSize>): Bundle {
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/TestUtils.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/TestUtils.kt
index 8151f2b..4927da4 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/TestUtils.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/TestUtils.kt
@@ -29,40 +29,67 @@
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import android.widget.FrameLayout
 import android.widget.RemoteViews
-import androidx.compose.runtime.BroadcastFrameClock
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composition
+import androidx.compose.runtime.MonotonicFrameClock
 import androidx.compose.runtime.Recomposer
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.TextUnit
 import androidx.core.view.children
 import androidx.glance.Applier
+import androidx.glance.GlanceComposable
+import androidx.glance.GlanceId
+import androidx.glance.session.GlobalSnapshotManager
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.currentCoroutineContext
 import kotlinx.coroutines.launch
 import java.util.Locale
+import java.util.concurrent.atomic.AtomicBoolean
 import kotlin.test.assertIs
+import kotlinx.coroutines.flow.first
 
-internal suspend fun runTestingComposition(content: @Composable () -> Unit): RemoteViewsRoot =
+internal suspend fun runTestingComposition(
+    content: @Composable @GlanceComposable () -> Unit,
+): RemoteViewsRoot =
+    runCompositionUntil(
+        stopWhen = { state: Recomposer.State, root: RemoteViewsRoot ->
+            state == Recomposer.State.Idle && !root.shouldIgnoreResult()
+        },
+        content
+    )
+
+internal suspend fun runCompositionUntil(
+    stopWhen: (Recomposer.State, RemoteViewsRoot) -> Boolean,
+    content: @Composable () -> Unit
+): RemoteViewsRoot =
     coroutineScope {
+        GlobalSnapshotManager.ensureStarted()
         val root = RemoteViewsRoot(10)
         val applier = Applier(root)
         val recomposer = Recomposer(currentCoroutineContext())
         val composition = Composition(applier, recomposer)
-        val frameClock = BroadcastFrameClock()
 
         composition.setContent { content() }
 
-        launch(frameClock) { recomposer.runRecomposeAndApplyChanges() }
+        launch(TestFrameClock()) { recomposer.runRecomposeAndApplyChanges() }
 
-        recomposer.close()
+        recomposer.currentState.first { stopWhen(it, root) }
+        recomposer.cancel()
         recomposer.join()
 
         root
     }
 
+/**
+ * Test clock that sends all frames immediately.
+ */
+class TestFrameClock : MonotonicFrameClock {
+    override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R) =
+        onFrame(System.currentTimeMillis())
+}
+
 /** Create the view out of a RemoteViews. */
 internal fun Context.applyRemoteViews(rv: RemoteViews): View {
     val p = Parcel.obtain()
@@ -143,9 +170,21 @@
     return children.mapNotNull { it.findView(predicate, klass) }.firstOrNull()
 }
 
-internal class TestWidget : GlanceAppWidget() {
+internal class TestWidget(
+    override val sizeMode: SizeMode = SizeMode.Single,
+    val ui: @Composable () -> Unit,
+) : GlanceAppWidget() {
+    val provideGlanceCalled = AtomicBoolean(false)
+    override suspend fun provideGlance(
+        context: android.content.Context,
+        id: androidx.glance.GlanceId
+    ) {
+        provideGlanceCalled.set(true)
+        provideContent { Content() }
+    }
     @Composable
     override fun Content() {
+        ui()
     }
 }
 
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/layout/LazyColumnTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/layout/LazyColumnTest.kt
index 32dfd75f..7da7812 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/layout/LazyColumnTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/layout/LazyColumnTest.kt
@@ -140,8 +140,8 @@
         val column = assertIs<EmittableLazyColumn>(root.children.single())
         val listItems = assertAre<EmittableLazyListItem>(column.children)
         assertThat(listItems[0].itemId).isEqualTo(5L)
-        assertThat(listItems[1].itemId).isEqualTo(ReservedItemIdRangeEnd)
-        assertThat(listItems[2].itemId).isEqualTo(ReservedItemIdRangeEnd - 1)
+        assertThat(listItems[1].itemId).isEqualTo(ReservedItemIdRangeEnd - 1)
+        assertThat(listItems[2].itemId).isEqualTo(ReservedItemIdRangeEnd - 2)
         assertThat(listItems[3].itemId).isEqualTo(6L)
     }
 
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/layout/LazyVerticalGridTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/layout/LazyVerticalGridTest.kt
index 06574d4..207f899 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/layout/LazyVerticalGridTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/layout/LazyVerticalGridTest.kt
@@ -149,8 +149,8 @@
         val verticalGrid = assertIs<EmittableLazyVerticalGrid>(root.children.single())
         val listItems = assertAre<EmittableLazyVerticalGridListItem>(verticalGrid.children)
         assertThat(listItems[0].itemId).isEqualTo(5L)
-        assertThat(listItems[1].itemId).isEqualTo(ReservedItemIdRangeEnd)
-        assertThat(listItems[2].itemId).isEqualTo(ReservedItemIdRangeEnd - 1)
+        assertThat(listItems[1].itemId).isEqualTo(ReservedItemIdRangeEnd - 1)
+        assertThat(listItems[2].itemId).isEqualTo(ReservedItemIdRangeEnd - 2)
         assertThat(listItems[3].itemId).isEqualTo(6L)
     }
 
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 734a6eb..0000000
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/CheckBoxTranslatorTest.kt
+++ /dev/null
@@ -1,224 +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.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.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()
-    }
-}
\ 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 9236064..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,14 +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.appwidget.applyRemoteViews
+import androidx.glance.ColorFilter
+import androidx.glance.GlanceModifier
+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
@@ -172,4 +180,97 @@
         assertThat(imageView.getContentDescription()).isEqualTo("oval")
         assertThat(imageView.getScaleType()).isEqualTo(ImageView.ScaleType.FIT_XY)
     }
+
+    @Test
+    fun translateImage_contentDescriptionFieldAndSemanticsSet_fieldPreferred() =
+        fakeCoroutineScope.runTest {
+            val rv = context.runAndTranslate {
+                Image(
+                    provider = ImageProvider(R.drawable.oval),
+                    contentDescription = "oval",
+                    modifier = GlanceModifier.semantics { contentDescription = "round" },
+                )
+            }
+
+            val imageView = assertIs<ImageView>(context.applyRemoteViews(rv))
+            assertThat(imageView.getContentDescription()).isEqualTo("oval")
+        }
+
+    @Test
+    fun translateImage_contentDescriptionFieldNullAndSemanticsSet_setFromSemantics() =
+        fakeCoroutineScope.runTest {
+            val rv = context.runAndTranslate {
+                Image(
+                    provider = ImageProvider(R.drawable.oval),
+                    contentDescription = null,
+                    modifier = GlanceModifier.semantics { contentDescription = "round" },
+                )
+            }
+
+            val imageView = assertIs<ImageView>(context.applyRemoteViews(rv))
+            assertThat(imageView.getContentDescription()).isEqualTo("round")
+        }
+
+    @Test
+    fun translateImage_contentDescriptionFieldAndSemanticsNull() =
+        fakeCoroutineScope.runTest {
+            val rv = context.runAndTranslate {
+                Image(
+                    provider = ImageProvider(R.drawable.oval),
+                    contentDescription = null,
+                    modifier = GlanceModifier.semantics {},
+                )
+            }
+
+            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 1f249fc..0000000
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/RadioButtonTranslatorTest.kt
+++ /dev/null
@@ -1,273 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.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.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.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()
-    }
-
-    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 3b70ee7..0000000
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/SwitchTranslatorTest.kt
+++ /dev/null
@@ -1,286 +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.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.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()
-    }
-
-    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/TextTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt
index 0d26478..b877f04 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt
@@ -45,6 +45,8 @@
 import androidx.glance.color.ColorProvider
 import androidx.glance.layout.Column
 import androidx.glance.layout.fillMaxWidth
+import androidx.glance.semantics.contentDescription
+import androidx.glance.semantics.semantics
 import androidx.glance.text.FontStyle
 import androidx.glance.text.FontWeight
 import androidx.glance.text.Text
@@ -371,6 +373,23 @@
         assertThat(view.maxLines).isEqualTo(5)
     }
 
+    @Test
+    fun canTranslateTextWithSemanticsModifier_contentDescription() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            Text(
+                text = "Max line is set",
+                maxLines = 5,
+                modifier = GlanceModifier.semantics {
+                    contentDescription = "Custom text description"
+                },
+            )
+        }
+        val view = context.applyRemoteViews(rv)
+
+        assertIs<TextView>(view)
+        assertThat(view.contentDescription).isEqualTo("Custom text description")
+    }
+
     // Check there is a single span, that it's of the correct type and passes the [check].
     private inline fun <reified T> SpannedString.checkSingleSpan(check: (T) -> Unit) {
         val spans = getSpans(0, length, Any::class.java)
diff --git a/glance/glance-wear-tiles/build.gradle b/glance/glance-wear-tiles/build.gradle
index 3b48948..f1ac6fa 100644
--- a/glance/glance-wear-tiles/build.gradle
+++ b/glance/glance-wear-tiles/build.gradle
@@ -33,7 +33,7 @@
     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)
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/WearCompositionTranslator.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
index 7e0ee8d..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
@@ -561,7 +562,7 @@
     val imageBuilder = LayoutElementBuilders.Image.Builder()
         .setWidth(element.modifier.getWidth(context).toImageDimension())
         .setHeight(element.modifier.getHeight(context).toImageDimension())
-        .setModifiers(translateModifiers(context, element.modifier, element.contentDescription))
+        .setModifiers(translateModifiers(context, element.modifier))
         .setResourceId(mappedResId)
         .setContentScaleMode(
             when (element.contentScale) {
@@ -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()
 }
 
@@ -701,7 +715,6 @@
 private fun translateModifiers(
     context: Context,
     modifier: GlanceModifier,
-    contentDescription: String? = null
 ): ModifiersBuilders.Modifiers =
     modifier.foldIn(ModifiersBuilders.Modifiers.Builder()) { builder, element ->
         when (element) {
@@ -727,14 +740,6 @@
                 ?.let {
                     builder.setPadding(it.toProto())
                 }
-
-            contentDescription?.let { contentDescription ->
-                builder.setSemantics(
-                    ModifiersBuilders.Semantics.Builder()
-                        .setContentDescription(contentDescription)
-                        .build()
-                )
-            }
         }
         .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 8b402b4..440722f 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/Image.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/Image.kt
@@ -24,6 +24,9 @@
 import androidx.annotation.RestrictTo
 import androidx.compose.runtime.Composable
 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.
@@ -74,24 +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 contentDescription: String? = null
+    var colorFilterParams: ColorFilterParams? = null
     var contentScale: ContentScale = ContentScale.Fit
 
     override fun copy(): Emittable = EmittableImage().also {
         it.modifier = modifier
         it.provider = provider
-        it.contentDescription = contentDescription
+        it.colorFilterParams = colorFilterParams
         it.contentScale = contentScale
     }
 
     override fun toString(): String = "EmittableImage(" +
         "modifier=$modifier, " +
         "provider=$provider, " +
-        "contentDescription=$contentDescription, " +
+        "colorFilterParams=$colorFilterParams, " +
         "contentScale=$contentScale" +
         ")"
 }
@@ -109,21 +137,31 @@
  * @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 {
+            this.contentDescription = contentDescription
+        }
+    } else {
+        modifier
+    }
+
     GlanceNode(
         factory = ::EmittableImage,
         update = {
             this.set(provider) { this.provider = it }
-            this.set(contentDescription) { this.contentDescription = it }
-            this.set(modifier) { this.modifier = 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/GlobalSnapshotManager.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/session/GlobalSnapshotManager.kt
index 7a9bba7..578fd4e 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/session/GlobalSnapshotManager.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/session/GlobalSnapshotManager.kt
@@ -16,6 +16,7 @@
 
 package androidx.glance.session
 
+import androidx.annotation.RestrictTo
 import androidx.compose.runtime.snapshots.Snapshot
 import java.util.concurrent.atomic.AtomicBoolean
 import kotlinx.coroutines.CoroutineScope
@@ -31,8 +32,10 @@
  * notifications (which are necessary in order for recompositions to be scheduled in response to
  * state changes). These will be sent on Dispatchers.Default.
  * This is based on [androidx.compose.ui.platform.GlobalSnapshotManager].
+ * @suppress
  */
-internal object GlobalSnapshotManager {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+object GlobalSnapshotManager {
     private val started = AtomicBoolean(false)
 
     fun ensureStarted() {
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 52010a0..28c7ad1 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/session/Session.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/session/Session.kt
@@ -23,7 +23,6 @@
 import androidx.glance.GlanceComposable
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.channels.ClosedReceiveChannelException
-import kotlinx.coroutines.flow.Flow
 
 typealias SetContentFn = suspend (@Composable @GlanceComposable () -> Unit) -> Unit
 
@@ -45,28 +44,24 @@
     /**
      * Provide the Glance composable to be run in the [androidx.compose.runtime.Composition].
      */
-    abstract suspend fun provideGlance(
-        context: Context,
-    ): Flow<@Composable @GlanceComposable () -> Unit>
+    abstract fun provideGlance(context: Context): @Composable @GlanceComposable () -> Unit
 
     /**
      * Process the Emittable tree that results from the running the composable provided by
      * [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,
-    )
+        root: EmittableWithChildren
+    ): Boolean
 
     /**
      * Process an event that was sent to this session.
      */
-    abstract suspend fun processEvent(
-        context: Context,
-        event: Any,
-    )
+    abstract suspend fun processEvent(context: Context, event: Any)
 
     /**
      * Enqueues an [event] to be processed by the session.
@@ -88,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 5ac0d49..5a5728e 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionWorker.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/session/SessionWorker.kt
@@ -20,9 +20,7 @@
 import android.util.Log
 import androidx.annotation.VisibleForTesting
 import androidx.compose.runtime.Composition
-import androidx.compose.runtime.RecomposeScope
 import androidx.compose.runtime.Recomposer
-import androidx.compose.runtime.currentRecomposeScope
 import androidx.glance.Applier
 import androidx.glance.EmittableWithChildren
 import androidx.work.CoroutineWorker
@@ -65,37 +63,32 @@
         GlobalSnapshotManager.ensureStarted()
         val root = session.createRootEmittable()
         val recomposer = Recomposer(coroutineContext)
-        val composition = Composition(Applier(root), recomposer)
-        val contentReady = MutableStateFlow(false)
-        val uiReady = MutableStateFlow(false)
-        val provideGlance = launch {
-            var recomposeScope: RecomposeScope? = null
-            session.provideGlance(applicationContext).collect { content ->
-                composition.setContent {
-                    recomposeScope = currentRecomposeScope
-                    content()
-                }
-                // Trigger recomposition. This is necessary when calling setContent multiple times.
-                recomposeScope?.invalidate()
-                contentReady.emit(true)
-            }
+        val composition = Composition(Applier(root), recomposer).apply {
+            setContent(session.provideGlance(applicationContext))
         }
+        val uiReady = MutableStateFlow(false)
 
-        contentReady.first { it }
         launch(frameClock) {
             recomposer.runRecomposeAndApplyChanges()
         }
         launch {
+            var lastRecomposeCount = recomposer.changeCount
             recomposer.currentState.collect { state ->
                 if (DEBUG) Log.d(TAG, "Recomposer(${session.key}): currentState=$state")
                 when (state) {
                     Recomposer.State.Idle -> {
-                        if (DEBUG) Log.d(TAG, "UI tree ready (${session.key})")
-                        session.processEmittableTree(
-                            applicationContext,
-                            root.copy() as EmittableWithChildren
-                        )
-                        uiReady.emit(true)
+                        // Only update the session when a change has actually occurred. The
+                        // Recomposer may sometimes wake up due to changes in other compositions.
+                        // 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})")
+                            val processed = session.processEmittableTree(
+                                applicationContext,
+                                root.copy() as EmittableWithChildren
+                            )
+                            if (!uiReady.value && processed) uiReady.emit(true)
+                        }
+                        lastRecomposeCount = recomposer.changeCount
                     }
                     Recomposer.State.ShutDown -> cancel()
                     else -> {}
@@ -103,6 +96,7 @@
             }
         }
 
+        // Wait until the Emittable tree has been processed at least once before receiving events.
         uiReady.first { it }
         session.receiveEvents(applicationContext) {
             if (DEBUG) Log.d(TAG, "processing event for ${session.key}")
@@ -110,7 +104,6 @@
         }
 
         composition.dispose()
-        provideGlance.cancel()
         frameClock.stopInteractive()
         recomposer.close()
         recomposer.join()
diff --git a/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt b/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt
index 0a67b2e..95473a6 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/ImageTest.kt
@@ -16,11 +16,15 @@
 
 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
 import androidx.glance.layout.padding
 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
@@ -28,6 +32,7 @@
 import org.junit.Before
 import org.junit.Test
 import kotlin.test.assertIs
+import kotlin.test.assertNotNull
 
 @OptIn(ExperimentalCoroutinesApi::class)
 class ImageTest {
@@ -56,8 +61,34 @@
 
         val imgSource = assertIs<AndroidResourceImageProvider>(img.provider)
         assertThat(imgSource.resId).isEqualTo(5)
-        assertThat(img.contentDescription).isEqualTo("Hello World")
+        val semanticsModifier = assertNotNull(img.modifier.findModifier<SemanticsModifier>())
+        assertThat(semanticsModifier.configuration[SemanticsProperties.ContentDescription])
+            .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 303d9ca..09d5058 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
@@ -31,7 +31,6 @@
 import com.google.common.truth.Truth.assertThat
 import kotlin.coroutines.suspendCoroutine
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.test.runTest
 import org.junit.After
 import org.junit.Before
@@ -47,13 +46,16 @@
             TODO("Not yet implemented")
         }
 
-        override suspend fun provideGlance(
+        override fun provideGlance(
             context: Context
-        ): Flow<@Composable @GlanceComposable () -> Unit> {
+        ): @Composable @GlanceComposable () -> Unit {
             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 f78bf74..208c6f1 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/session/SessionWorkerTest.kt
@@ -33,17 +33,17 @@
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.assertIs
 import kotlin.test.assertNotNull
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(RobolectricTestRunner::class)
 class SessionWorkerTest {
     private val sessionManager = TestSessionManager()
@@ -62,7 +62,7 @@
     }
 
     @Test
-    fun createSessionWorker() = runBlocking {
+    fun createSessionWorker() = runTest {
         launch {
             val result = worker.doWork()
             assertThat(result).isEqualTo(Result.success())
@@ -72,7 +72,7 @@
     }
 
     @Test
-    fun sessionWorkerRunsComposition() = runBlocking {
+    fun sessionWorkerRunsComposition() = runTest {
         launch {
             val result = worker.doWork()
             assertThat(result).isEqualTo(Result.success())
@@ -90,7 +90,7 @@
     }
 
     @Test
-    fun sessionWorkerCallsProvideGlance(): Unit = runBlocking {
+    fun sessionWorkerCallsProvideGlance(): Unit = runTest {
         launch {
             val result = worker.doWork()
             assertThat(result).isEqualTo(Result.success())
@@ -102,7 +102,7 @@
     }
 
     @Test
-    fun sessionWorkerStateChangeTriggersRecomposition() = runBlocking {
+    fun sessionWorkerStateChangeTriggersRecomposition() = runTest {
         launch {
             val result = worker.doWork()
             assertThat(result).isEqualTo(Result.success())
@@ -126,7 +126,7 @@
     }
 
     @Test
-    fun sessionWorkerReceivesActions() = runBlocking {
+    fun sessionWorkerReceivesActions() = runTest {
         launch {
             val result = worker.doWork()
             assertThat(result).isEqualTo(Result.success())
@@ -195,15 +195,19 @@
     }
 
     var provideGlanceCalled = 0
-    override suspend fun provideGlance(
+    override fun provideGlance(
         context: Context
-    ): Flow<@Composable @GlanceComposable () -> Unit> {
+    ): @Composable @GlanceComposable () -> Unit {
         provideGlanceCalled++
-        return flowOf(content)
+        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/OWNERS b/graphics/filters/OWNERS
index 1d01c7c..6c8a8b5 100644
--- a/graphics/filters/OWNERS
+++ b/graphics/filters/OWNERS
@@ -3,4 +3,5 @@
 chet@google.com
 xxayedawgxx@google.com
 andrewlewis@google.com
-huangdarwin@google.com
\ No newline at end of file
+huangdarwin@google.com
+chpitts@google.com
diff --git a/graphics/filters/filters/build.gradle b/graphics/filters/filters/build.gradle
index d61d155..745c9a0 100644
--- a/graphics/filters/filters/build.gradle
+++ b/graphics/filters/filters/build.gradle
@@ -23,7 +23,19 @@
 }
 
 dependencies {
+    def media3Version = '1.0.0-beta03'
+
     api(libs.kotlinStdlib)
+
+    // 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)
@@ -32,6 +44,10 @@
 }
 
 android {
+    defaultConfig {
+        minSdkVersion 21
+    }
+
     namespace "androidx.graphics.filters"
 }
 
diff --git a/graphics/filters/filters/src/androidTest/AndroidManifest.xml b/graphics/filters/filters/src/androidTest/AndroidManifest.xml
index 8855711..f31f8d9 100644
--- a/graphics/filters/filters/src/androidTest/AndroidManifest.xml
+++ b/graphics/filters/filters/src/androidTest/AndroidManifest.xml
@@ -16,15 +16,18 @@
   -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+
     <application>
         <activity android:name="androidx.graphics.filters.TestFiltersActivity"
-            android:label="Graphics Filters"
+            android:label="Video Filter"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
-
     </application>
 </manifest>
diff --git a/graphics/filters/filters/src/androidTest/kotlin/androidx/graphics/filters/TestFiltersActivity.kt b/graphics/filters/filters/src/androidTest/kotlin/androidx/graphics/filters/TestFiltersActivity.kt
index 18c0b07..fbc57cf 100644
--- a/graphics/filters/filters/src/androidTest/kotlin/androidx/graphics/filters/TestFiltersActivity.kt
+++ b/graphics/filters/filters/src/androidTest/kotlin/androidx/graphics/filters/TestFiltersActivity.kt
@@ -16,31 +16,288 @@
 
 package androidx.graphics.filters
 
+import android.Manifest.permission.READ_EXTERNAL_STORAGE
 import android.app.Activity
+import android.content.pm.PackageManager
+import android.net.Uri
 import android.os.Bundle
+import android.os.Handler
 import android.view.Gravity
+import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.MATCH_PARENT
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.Button
 import android.widget.FrameLayout
+import android.widget.LinearLayout
+import android.widget.ScrollView
 import android.widget.TextView
+import androidx.media3.common.Effect
+import androidx.media3.common.MediaItem
+import androidx.media3.common.MediaLibraryInfo.TAG
+import androidx.media3.common.util.Log
+import androidx.media3.common.util.Util
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.transformer.DefaultEncoderFactory
+import androidx.media3.transformer.ProgressHolder
+import androidx.media3.transformer.TransformationException
+import androidx.media3.transformer.TransformationRequest
+import androidx.media3.transformer.TransformationResult
+import androidx.media3.transformer.Transformer
+import androidx.media3.ui.PlayerView
+import com.google.common.collect.ImmutableList
+import java.io.File
+import java.io.IOException
+
+private val PRESET_FILE_URIS =
+  arrayOf(
+    "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4",
+    "https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4",
+    "https://html5demos.com/assets/dizzy.mp4",
+    "https://html5demos.com/assets/dizzy.webm",
+    "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_4k60.mp4",
+    "https://storage.googleapis.com/exoplayer-test-media-1/mp4/8k24fps_4s.mp4",
+    "https://storage.googleapis.com/exoplayer-test-media-1/mp4/1920w_1080h_4s.mp4",
+    "https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4",
+    "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_avc_aac.mp4",
+    "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_rotated_avc_aac.mp4",
+    "https://storage.googleapis.com/exoplayer-test-media-internal-63834241aced7884c2544af1a" +
+      "3452e01/mp4/slow%20motion/slowMotion_countdown_120fps.mp4",
+    "https://storage.googleapis.com/exoplayer-test-media-1/mp4/slow-motion/" +
+      "slowMotion_stopwatch_240fps_long.mp4",
+    "https://storage.googleapis.com/exoplayer-test-media-1/gen/screens/dash-vod-single-segment/" +
+      "manifest-baseline.mpd",
+    "https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-s21-hdr-hdr10.mp4",
+    "https://storage.googleapis.com/exoplayer-test-media-1/mp4/Pixel7Pro_HLG_1080P.mp4",
+    "https://storage.googleapis.com/exoplayer-test-media-internal-63834241aced7884c2544af1a3452" +
+      "e01/mp4/sony-hdr-hlg-full-range.mp4"
+  )
 
 class TestFiltersActivity : Activity() {
 
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        setContentView(
-            FrameLayout(this).apply {
-                layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+  private var outputFile: File? = null
+  private var transformer: Transformer? = null
+  private var sourcePlayer: ExoPlayer? = null
+  private var filteredPlayer: ExoPlayer? = null
+  private var sourcePlayerView: PlayerView? = null
+  private var filteredPlayerView: PlayerView? = null
+  private var filterButton: Button? = null
+  private var statusBar: TextView? = null
+
+  override fun onCreate(savedInstanceState: Bundle?) {
+    super.onCreate(savedInstanceState)
+
+    sourcePlayerView = PlayerView(this@TestFiltersActivity)
+    sourcePlayerView!!.minimumHeight = 480
+    sourcePlayerView!!.minimumWidth = 640
+    sourcePlayerView!!.layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
+    filteredPlayerView = PlayerView(this@TestFiltersActivity)
+    filteredPlayerView!!.minimumHeight = 480
+    filteredPlayerView!!.minimumWidth = 640
+    filteredPlayerView!!.layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
+
+    statusBar = TextView(this)
+
+    setContentView(
+      FrameLayout(this).apply {
+        layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+
+        addView(
+          ScrollView(this@TestFiltersActivity).apply {
+            layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
+            addView(
+              LinearLayout(this@TestFiltersActivity).apply {
+                layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
+                orientation = LinearLayout.VERTICAL
                 addView(
-                    TextView(this@TestFiltersActivity).apply {
-                        layoutParams = FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
-                            gravity = Gravity.CENTER
-                        }
-                        text = "Hello filters!"
-                    }
+                  TextView(this@TestFiltersActivity).apply {
+                    layoutParams =
+                      LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
+                        gravity = Gravity.LEFT
+                      }
+                    text = "Source Video"
+                  }
                 )
-            }
+                addView(sourcePlayerView)
+                addView(
+                  TextView(this@TestFiltersActivity).apply {
+                    layoutParams =
+                      LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply {
+                        gravity = Gravity.LEFT
+                      }
+                    text = "Filtered Video"
+                  }
+                )
+                addView(filteredPlayerView)
+                addView(statusBar)
+                addView(createControls())
+              }
+            )
+          }
         )
+      }
+    )
+  }
+
+  private fun createControls(): View {
+    this.filterButton = Button(this)
+    this.filterButton!!.text = "Run Filter"
+    this.filterButton!!.setOnClickListener(
+      View.OnClickListener { this@TestFiltersActivity.startTransformation() }
+    )
+
+    val controls =
+      LinearLayout(this).apply {
+        layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
+        orientation = LinearLayout.HORIZONTAL
+
+        addView(this@TestFiltersActivity.filterButton)
+      }
+
+    return controls
+  }
+
+  private fun startTransformation() {
+    statusBar!!.text = "Request permissions"
+    requestPermission()
+    statusBar!!.text = "Setup transformation"
+
+    val mediaUri = Uri.parse(PRESET_FILE_URIS[0])
+    try {
+      outputFile = createExternalCacheFile("filters-output.mp4")
+      val outputFilePath: String = outputFile!!.getAbsolutePath()
+      val mediaItem: MediaItem = createMediaItem(mediaUri)
+      var transformer: Transformer = createTransformer(outputFilePath)
+      transformer.startTransformation(mediaItem, outputFilePath)
+      this.transformer = transformer
+    } catch (e: IOException) {
+      throw IllegalStateException(e)
     }
-}
\ No newline at end of file
+    val mainHandler = Handler(mainLooper)
+    val progressHolder = ProgressHolder()
+    mainHandler.post(
+      object : Runnable {
+        override fun run() {
+          if (
+            transformer?.getProgress(progressHolder) !=
+              Transformer.PROGRESS_STATE_NO_TRANSFORMATION
+          ) {
+            mainHandler.postDelayed(/* r= */ this, /* delayMillis= */ 500)
+          }
+        }
+      }
+    )
+  }
+
+  private fun createTransformer(filePath: String): Transformer {
+    val transformerBuilder = Transformer.Builder(/* context= */ this)
+    val effects: List<Effect> = createEffectsList()
+
+    val requestBuilder = TransformationRequest.Builder()
+    transformerBuilder
+      .setTransformationRequest(requestBuilder.build())
+      .setEncoderFactory(
+        DefaultEncoderFactory.Builder(this.applicationContext).setEnableFallback(false).build()
+      )
+    transformerBuilder.setVideoEffects(effects)
+
+    return transformerBuilder
+      .addListener(
+        object : Transformer.Listener {
+          override fun onTransformationCompleted(
+            mediaItem: MediaItem,
+            transformationResult: TransformationResult
+          ) {
+            this@TestFiltersActivity.onTransformationCompleted(filePath, mediaItem)
+          }
+
+          override fun onTransformationError(
+            mediaItem: MediaItem,
+            exception: TransformationException
+          ) {
+            this@TestFiltersActivity.onTransformationError(exception)
+          }
+        }
+      )
+      .build()
+  }
+
+  private fun onTransformationError(exception: TransformationException) {
+    statusBar!!.text = "Transformation error: " + exception.message
+    Log.e(TAG, "Transformation error", exception)
+  }
+
+  private fun onTransformationCompleted(filePath: String, inputMediaItem: MediaItem?) {
+    statusBar!!.text = "Transformation success!"
+    Log.d(TAG, "Output file path: file://$filePath")
+    playMediaItems(inputMediaItem, MediaItem.fromUri("file://" + filePath))
+  }
+
+  private fun playMediaItems(inputMediaItem: MediaItem?, outputMediaItem: MediaItem) {
+    sourcePlayerView!!.player = null
+    filteredPlayerView!!.player = null
+
+    releasePlayer()
+    var sourcePlayer = ExoPlayer.Builder(/* context= */ this).build()
+    sourcePlayerView!!.player = sourcePlayer
+    sourcePlayerView!!.controllerAutoShow = false
+    if (inputMediaItem != null) {
+      sourcePlayer.setMediaItem(inputMediaItem)
+    }
+    sourcePlayer.prepare()
+    this.sourcePlayer = sourcePlayer
+    sourcePlayer.volume = 0f
+    var filteredPlayer = ExoPlayer.Builder(/* context= */ this).build()
+    filteredPlayerView!!.player = filteredPlayer
+    filteredPlayerView!!.controllerAutoShow = false
+    filteredPlayer.setMediaItem(outputMediaItem)
+    filteredPlayer.prepare()
+    this.filteredPlayer = filteredPlayer
+    sourcePlayer.play()
+    filteredPlayer.play()
+  }
+
+  private fun releasePlayer() {
+    if (sourcePlayer != null) {
+      sourcePlayer!!.release()
+      sourcePlayer = null
+    }
+    if (filteredPlayer != null) {
+      filteredPlayer!!.release()
+      filteredPlayer = null
+    }
+  }
+
+  private fun createEffectsList(): List<Effect> {
+    val effects = ImmutableList.Builder<Effect>()
+
+    effects.add(Vignette(0.5f, 0.75f))
+
+    return effects.build()
+  }
+
+  private fun requestPermission() {
+    if (Util.SDK_INT < 23) {
+      return
+    }
+
+    if (checkSelfPermission(READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+      requestPermissions(Array<String>(1) { READ_EXTERNAL_STORAGE }, /* requestCode= */ 0)
+    }
+  }
+
+  @Throws(IOException::class)
+  private fun createExternalCacheFile(fileName: String): File? {
+    val file = File(externalCacheDir, fileName)
+    check(!(file.exists() && !file.delete())) {
+      "Could not delete the previous transformer output file"
+    }
+    check(file.createNewFile()) { "Could not create the transformer output file" }
+    return file
+  }
+
+  private fun createMediaItem(uri: Uri): MediaItem {
+    val mediaItemBuilder = MediaItem.Builder().setUri(uri)
+    return mediaItemBuilder.build()
+  }
+}
diff --git a/graphics/filters/filters/src/main/assets/shaders/fragment_shader_vignette_es2.glsl b/graphics/filters/filters/src/main/assets/shaders/fragment_shader_vignette_es2.glsl
new file mode 100644
index 0000000..94012b8
--- /dev/null
+++ b/graphics/filters/filters/src/main/assets/shaders/fragment_shader_vignette_es2.glsl
@@ -0,0 +1,67 @@
+#version 100
+// Copyright 2022 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// ES2 Vignetting fragment shader
+precision highp float;
+uniform sampler2D uTexSampler;
+
+uniform int uShouldVignetteColor;
+uniform int uShouldVignetteAlpha;
+uniform float uInnerRadius;
+uniform float uOuterRadius;
+uniform float uAspectRatio;
+varying vec2 vTexSamplingCoord;
+
+void main() {
+    vec4 inputColor = texture2D(uTexSampler, vTexSamplingCoord);
+
+    // Update x and y coords to [-1, 1]
+    float recenteredX = 2.0 * (vTexSamplingCoord.x - 0.5);
+    float recenteredY = 2.0 * (vTexSamplingCoord.y - 0.5);
+
+    // Un-normalize y coord so that the size of units match on x and y axes, based on an x-axis
+    // with data in [-1, 1]
+    float aspectCorrectY = recenteredY / uAspectRatio;
+    float invAspectRatio = 1.0 / uAspectRatio;
+    // Calculate maxRadius, the distance from the center to a corner of the image.
+    float maxRadius = sqrt(1.0 + invAspectRatio * invAspectRatio);
+    // Calculate radial position of the current texture coordinate.
+    float radius = sqrt(recenteredX * recenteredX + aspectCorrectY * aspectCorrectY);
+    // Normalize the radius based on the distance to the corner.
+    float normalizedRadius = radius / maxRadius;
+
+    // Calculate the vignette amount.  Data outside of the outer radius is set to 0.  Data inside of
+    // the inner radius is unchanged.  Data between is interpolated linearly.
+    float vignetteAmount = 0.0;
+    if (normalizedRadius > uOuterRadius) {
+        vignetteAmount = 1.0;
+    } else if (normalizedRadius > uInnerRadius) {
+        vignetteAmount = (normalizedRadius - uInnerRadius) / (uOuterRadius - uInnerRadius);
+    }
+
+    // Apply the vignetting.
+    gl_FragColor.rgba = inputColor.rgba;
+
+    bool vignetteAlpha = uShouldVignetteAlpha > 0;
+    bool vignetteColor = uShouldVignetteColor > 0;
+
+    if (vignetteColor && vignetteAlpha) {
+        gl_FragColor.rgba = inputColor.rgba * (1.0 - vignetteAmount);
+    } else if (vignetteColor) {
+        gl_FragColor.rgb = inputColor.rgb * (1.0 - vignetteAmount);
+    } else if (vignetteAlpha) {
+        gl_FragColor.a = inputColor.a * (1.0 - vignetteAmount);
+    }
+}
\ No newline at end of file
diff --git a/graphics/filters/filters/src/main/assets/shaders/vertex_shader_transformation_es2.glsl b/graphics/filters/filters/src/main/assets/shaders/vertex_shader_transformation_es2.glsl
new file mode 100644
index 0000000..791e52e
--- /dev/null
+++ b/graphics/filters/filters/src/main/assets/shaders/vertex_shader_transformation_es2.glsl
@@ -0,0 +1,27 @@
+#version 100
+// 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.
+
+// ES 2 vertex shader that applies the 4 * 4 transformation matrices
+// uTransformationMatrix and the uTexTransformationMatrix.
+
+attribute vec4 aFramePosition;
+uniform mat4 uTransformationMatrix;
+uniform mat4 uTexTransformationMatrix;
+varying vec2 vTexSamplingCoord;
+void main() {
+    gl_Position = uTransformationMatrix * aFramePosition;
+    vec4 texturePosition = vec4(aFramePosition.x * 0.5 + 0.5, aFramePosition.y * 0.5 + 0.5, 0.0, 1.0);
+    vTexSamplingCoord = (uTexTransformationMatrix * texturePosition).xy;
+}
\ No newline at end of file
diff --git a/graphics/filters/filters/src/main/java/androidx/graphics/filters/Vignette.kt b/graphics/filters/filters/src/main/java/androidx/graphics/filters/Vignette.kt
new file mode 100644
index 0000000..9bf46f3
--- /dev/null
+++ b/graphics/filters/filters/src/main/java/androidx/graphics/filters/Vignette.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.filters
+
+import android.content.Context
+import androidx.annotation.FloatRange
+import androidx.media3.common.FrameProcessingException
+import androidx.media3.common.util.Assertions
+import androidx.media3.effect.GlEffect
+import androidx.media3.effect.SingleFrameGlTextureProcessor
+
+/** A {@link androidx.media3.common.GlEffect} to apply a vignetting effect to image data.
+ * The vignetting may be applied to color and/or alpha channels.
+ *
+ * Inner radius and outer radius are in [0,1], normalized by the distance from the center of the
+ * image to a corner.  A pixels within the inner radius are unaffected, while affected pixel values
+ * outside the outer radius are set to 0.  Pixels between the inner and outer radius are
+ * interpolated linearly.
+ *
+ * @param innerRadius Radius in [0,1], normalized by the distance from image center to corner. All
+ *        pixels within innerRadius are unaffected by the vignette.
+ * @param outerRadius Radius in [0,1], normalized by the distance from image center to corner. All
+ *        pixels outside of outerRadius are fully vignetted.
+ * @param vignetteStyle Enum indicating to which channels vignetting should be applied.
+ */
+internal class Vignette(
+  @FloatRange(from = 0.0, to = 1.0) innerRadius: Float,
+  @FloatRange(from = 0.0, to = 1.0) outerRadius: Float,
+  vignetteStyle: VignetteStyle = VignetteStyle.COLOR,
+) : GlEffect {
+
+  enum class VignetteStyle {
+    COLOR, // Vignetting is applied to color channels only.
+    ALPHA, // Vignetting is applied to alpha channel only.
+    COLOR_AND_ALPHA // Vignetting is applied to color and alpha channels.
+  }
+
+  private val mInnerRadius: Float
+  val innerRadius get() = this.mInnerRadius
+
+  private val mOuterRadius: Float
+  val outerRadius get() = this.mOuterRadius
+
+  private val mVignetteStyle: VignetteStyle
+  val vignetteStyle get() = this.mVignetteStyle
+
+  init {
+    Assertions.checkArgument(
+      innerRadius in 0.0..1.0,
+      "InnerRadius needs to be in the interval [0, 1]."
+    )
+    Assertions.checkArgument(
+      outerRadius in innerRadius..1.0F,
+      "InnerRadius needs to be in the interval [innerRadius, 1]."
+    )
+
+    this.mInnerRadius = innerRadius
+    this.mOuterRadius = outerRadius
+    this.mVignetteStyle = vignetteStyle
+  }
+
+  // media3 GlEffect does not annotate nullability of toGlTextureProcessor.  Cannot override
+  // toGlTextureProcessor and satisfy API lint simultaneously.
+  @Throws(FrameProcessingException::class)
+  override fun toGlTextureProcessor(
+    @Suppress("InvalidNullabilityOverride") // Remove when b/264908709 is resolved.
+    context: Context,
+    useHdr: Boolean
+  ): SingleFrameGlTextureProcessor {
+    return VignetteProcessor(context, this, useHdr)
+  }
+}
diff --git a/graphics/filters/filters/src/main/java/androidx/graphics/filters/VignetteProcessor.kt b/graphics/filters/filters/src/main/java/androidx/graphics/filters/VignetteProcessor.kt
new file mode 100644
index 0000000..2ab3d6d
--- /dev/null
+++ b/graphics/filters/filters/src/main/java/androidx/graphics/filters/VignetteProcessor.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.filters
+
+import android.content.Context
+import android.opengl.GLES20
+import android.util.Pair
+import androidx.media3.common.FrameProcessingException
+import androidx.media3.common.util.GlProgram
+import androidx.media3.common.util.GlUtil
+import androidx.media3.effect.SingleFrameGlTextureProcessor
+import java.io.IOException
+
+/** Applies a {@link Vignette} effect to each frame in the fragment shader. */
+
+/**
+ * Creates a new instance.
+ *
+ * @param context The {@link Context}.
+ * @param vignetteEffect The {@link Vignette} to apply to each frame in order.
+ * @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be
+ *     in linear RGB BT.2020. If {@code false}, colors will be in linear RGB BT.709.
+ * @throws FrameProcessingException If a problem occurs while reading shader files.
+ */
+internal class VignetteProcessor(context: Context?, vignetteEffect: Vignette, useHdr: Boolean) :
+  SingleFrameGlTextureProcessor(useHdr) {
+  private var glProgram: GlProgram? = null
+  private var aspectRatio: Float = 0.0f
+
+  private val vignetteEffect: Vignette
+
+  init {
+    this.vignetteEffect = vignetteEffect
+
+    glProgram =
+      try {
+        GlProgram(context!!, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH)
+      } catch (e: IOException) {
+        throw FrameProcessingException(e)
+      } catch (e: GlUtil.GlException) {
+        throw FrameProcessingException(e)
+      }
+
+    // Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
+    glProgram!!.setBufferAttribute(
+      "aFramePosition",
+      GlUtil.getNormalizedCoordinateBounds(),
+      GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE
+    )
+    val identityMatrix = GlUtil.create4x4IdentityMatrix()
+    glProgram!!.setFloatsUniform("uTransformationMatrix", identityMatrix)
+    glProgram!!.setFloatsUniform("uTexTransformationMatrix", identityMatrix)
+  }
+
+  override fun configure(inputWidth: Int, inputHeight: Int): Pair<Int, Int> {
+    this.aspectRatio = inputWidth.toFloat() / inputHeight.toFloat()
+
+    return Pair.create(inputWidth, inputHeight)
+  }
+
+  @Throws(FrameProcessingException::class)
+  override fun drawFrame(inputTexId: Int, presentationTimeUs: Long) {
+    try {
+      glProgram!!.use()
+      // Set the various uniform right before rendering.  This allows values to
+      // be changed or interpolated between frames.
+
+      val shouldVignetteAlpha = this.vignetteEffect.vignetteStyle == Vignette.VignetteStyle.ALPHA ||
+          this.vignetteEffect.vignetteStyle == Vignette.VignetteStyle.COLOR_AND_ALPHA
+      val shouldVignetteColor = this.vignetteEffect.vignetteStyle == Vignette.VignetteStyle.COLOR ||
+          this.vignetteEffect.vignetteStyle == Vignette.VignetteStyle.COLOR_AND_ALPHA
+      glProgram!!.setFloatUniform("uAspectRatio", this.aspectRatio)
+      glProgram!!.setFloatUniform("uInnerRadius", this.vignetteEffect.innerRadius)
+      glProgram!!.setFloatUniform("uOuterRadius", this.vignetteEffect.outerRadius)
+      glProgram!!.setIntUniform("uShouldVignetteAlpha",
+        if (shouldVignetteAlpha) 1 else 0)
+      glProgram!!.setIntUniform("uShouldVignetteColor",
+        if (shouldVignetteColor) 1 else 0)
+      glProgram!!.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0)
+      glProgram!!.bindAttributesAndUniforms()
+
+      // The four-vertex triangle strip forms a quad.
+      GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4)
+    } catch (e: GlUtil.GlException) {
+      throw FrameProcessingException(e, presentationTimeUs)
+    }
+  }
+
+  @Throws(FrameProcessingException::class)
+  override fun release() {
+    super.release()
+    try {
+      glProgram!!.delete()
+    } catch (e: GlUtil.GlException) {
+      throw FrameProcessingException(e)
+    }
+  }
+
+  companion object {
+    private const val VERTEX_SHADER_PATH = "shaders/vertex_shader_transformation_es2.glsl"
+    private const val FRAGMENT_SHADER_PATH = "shaders/fragment_shader_vignette_es2.glsl"
+  }
+}
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 ab28109..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
-    @SdkSuppress(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
+    @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(maxSdkVersion = 33) // b/262909049: Failing on SDK 34
+    @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 3282fb7..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,24 +24,24 @@
   }
 
   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);
   }
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface PermissionController {
-    method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract(optional String providerPackageName);
-    method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract();
-    method public suspend Object? getGrantedPermissions(java.util.Set<androidx.health.connect.client.permission.HealthPermission> permissions, kotlin.coroutines.Continuation<? super java.util.Set<? extends androidx.health.connect.client.permission.HealthPermission>>);
+    method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
+    method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract();
+    method public suspend Object? getGrantedPermissions(kotlin.coroutines.Continuation<? super java.util.Set<? extends java.lang.String>>);
     method public suspend Object? revokeAllPermissions(kotlin.coroutines.Continuation<? super kotlin.Unit>);
     field public static final androidx.health.connect.client.PermissionController.Companion Companion;
   }
 
   public static final class PermissionController.Companion {
-    method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract(optional String providerPackageName);
-    method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract();
+    method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
+    method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract();
   }
 
 }
@@ -102,14 +102,14 @@
 package androidx.health.connect.client.permission {
 
   public final class HealthPermission {
-    method public static androidx.health.connect.client.permission.HealthPermission createReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
-    method public static androidx.health.connect.client.permission.HealthPermission createWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
+    method public static String getReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
+    method public static String getWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
     field public static final androidx.health.connect.client.permission.HealthPermission.Companion Companion;
   }
 
   public static final class HealthPermission.Companion {
-    method public androidx.health.connect.client.permission.HealthPermission createReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
-    method public androidx.health.connect.client.permission.HealthPermission createWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
+    method public String getReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
+    method public String getWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
   }
 
 }
@@ -407,98 +407,6 @@
   public static final class ElevationGainedRecord.Companion {
   }
 
-  public final class ExerciseEventRecord implements androidx.health.connect.client.records.Record {
-    ctor public ExerciseEventRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int eventType, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public java.time.Instant getEndTime();
-    method public java.time.ZoneOffset? getEndZoneOffset();
-    method public int getEventType();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getStartTime();
-    method public java.time.ZoneOffset? getStartZoneOffset();
-    property public java.time.Instant endTime;
-    property public java.time.ZoneOffset? endZoneOffset;
-    property public final int eventType;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant startTime;
-    property public java.time.ZoneOffset? startZoneOffset;
-    field public static final androidx.health.connect.client.records.ExerciseEventRecord.Companion Companion;
-    field public static final int EVENT_TYPE_PAUSE = 1; // 0x1
-    field public static final int EVENT_TYPE_REST = 2; // 0x2
-    field public static final int EVENT_TYPE_UNKNOWN = 0; // 0x0
-  }
-
-  public static final class ExerciseEventRecord.Companion {
-  }
-
-  public final class ExerciseLapRecord implements androidx.health.connect.client.records.Record {
-    ctor public ExerciseLapRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.units.Length? length, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public java.time.Instant getEndTime();
-    method public java.time.ZoneOffset? getEndZoneOffset();
-    method public androidx.health.connect.client.units.Length? getLength();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getStartTime();
-    method public java.time.ZoneOffset? getStartZoneOffset();
-    property public java.time.Instant endTime;
-    property public java.time.ZoneOffset? endZoneOffset;
-    property public final androidx.health.connect.client.units.Length? length;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant startTime;
-    property public java.time.ZoneOffset? startZoneOffset;
-  }
-
-  public final class ExerciseRepetitionsRecord implements androidx.health.connect.client.records.Record {
-    ctor public ExerciseRepetitionsRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, long count, int type, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public long getCount();
-    method public java.time.Instant getEndTime();
-    method public java.time.ZoneOffset? getEndZoneOffset();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getStartTime();
-    method public java.time.ZoneOffset? getStartZoneOffset();
-    method public int getType();
-    property public final long count;
-    property public java.time.Instant endTime;
-    property public java.time.ZoneOffset? endZoneOffset;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant startTime;
-    property public java.time.ZoneOffset? startZoneOffset;
-    property public final int type;
-    field public static final androidx.health.connect.client.records.ExerciseRepetitionsRecord.Companion Companion;
-    field public static final int REPETITION_TYPE_ARM_CURL = 1; // 0x1
-    field public static final int REPETITION_TYPE_BACK_EXTENSION = 2; // 0x2
-    field public static final int REPETITION_TYPE_BALL_SLAM = 3; // 0x3
-    field public static final int REPETITION_TYPE_BENCH_PRESS = 4; // 0x4
-    field public static final int REPETITION_TYPE_BURPEE = 5; // 0x5
-    field public static final int REPETITION_TYPE_CRUNCH = 6; // 0x6
-    field public static final int REPETITION_TYPE_DEADLIFT = 7; // 0x7
-    field public static final int REPETITION_TYPE_DOUBLE_ARM_TRICEPS_EXTENSION = 8; // 0x8
-    field public static final int REPETITION_TYPE_DUMBBELL_ROW = 9; // 0x9
-    field public static final int REPETITION_TYPE_FRONT_RAISE = 10; // 0xa
-    field public static final int REPETITION_TYPE_HIP_THRUST = 11; // 0xb
-    field public static final int REPETITION_TYPE_HULA_HOOP = 12; // 0xc
-    field public static final int REPETITION_TYPE_JUMPING_JACK = 13; // 0xd
-    field public static final int REPETITION_TYPE_JUMP_ROPE = 14; // 0xe
-    field public static final int REPETITION_TYPE_KETTLEBELL_SWING = 15; // 0xf
-    field public static final int REPETITION_TYPE_LATERAL_RAISE = 16; // 0x10
-    field public static final int REPETITION_TYPE_LAT_PULL_DOWN = 17; // 0x11
-    field public static final int REPETITION_TYPE_LEG_CURL = 18; // 0x12
-    field public static final int REPETITION_TYPE_LEG_EXTENSION = 19; // 0x13
-    field public static final int REPETITION_TYPE_LEG_PRESS = 20; // 0x14
-    field public static final int REPETITION_TYPE_LEG_RAISE = 21; // 0x15
-    field public static final int REPETITION_TYPE_LUNGE = 22; // 0x16
-    field public static final int REPETITION_TYPE_MOUNTAIN_CLIMBER = 23; // 0x17
-    field public static final int REPETITION_TYPE_PLANK = 24; // 0x18
-    field public static final int REPETITION_TYPE_PULL_UP = 25; // 0x19
-    field public static final int REPETITION_TYPE_PUNCH = 26; // 0x1a
-    field public static final int REPETITION_TYPE_SHOULDER_PRESS = 27; // 0x1b
-    field public static final int REPETITION_TYPE_SINGLE_ARM_TRICEPS_EXTENSION = 28; // 0x1c
-    field public static final int REPETITION_TYPE_SIT_UP = 29; // 0x1d
-    field public static final int REPETITION_TYPE_SQUAT = 30; // 0x1e
-    field public static final int REPETITION_TYPE_UNKNOWN = 0; // 0x0
-  }
-
-  public static final class ExerciseRepetitionsRecord.Companion {
-  }
-
   public final class ExerciseSessionRecord implements androidx.health.connect.client.records.Record {
     ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
@@ -1176,34 +1084,6 @@
   public static final class StepsRecord.Companion {
   }
 
-  public final class SwimmingStrokesRecord implements androidx.health.connect.client.records.Record {
-    ctor public SwimmingStrokesRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int type, optional long count, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public long getCount();
-    method public java.time.Instant getEndTime();
-    method public java.time.ZoneOffset? getEndZoneOffset();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getStartTime();
-    method public java.time.ZoneOffset? getStartZoneOffset();
-    method public int getType();
-    property public final long count;
-    property public java.time.Instant endTime;
-    property public java.time.ZoneOffset? endZoneOffset;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant startTime;
-    property public java.time.ZoneOffset? startZoneOffset;
-    property public final int type;
-    field public static final androidx.health.connect.client.records.SwimmingStrokesRecord.Companion Companion;
-    field public static final int SWIMMING_TYPE_BACKSTROKE = 2; // 0x2
-    field public static final int SWIMMING_TYPE_BREASTSTROKE = 3; // 0x3
-    field public static final int SWIMMING_TYPE_BUTTERFLY = 4; // 0x4
-    field public static final int SWIMMING_TYPE_FREESTYLE = 1; // 0x1
-    field public static final int SWIMMING_TYPE_MIXED = 5; // 0x5
-    field public static final int SWIMMING_TYPE_OTHER = 0; // 0x0
-  }
-
-  public static final class SwimmingStrokesRecord.Companion {
-  }
-
   public final class TotalCaloriesBurnedRecord implements androidx.health.connect.client.records.Record {
     ctor public TotalCaloriesBurnedRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, androidx.health.connect.client.units.Energy energy, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
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 3282fb7..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,24 +24,24 @@
   }
 
   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);
   }
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface PermissionController {
-    method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract(optional String providerPackageName);
-    method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract();
-    method public suspend Object? getGrantedPermissions(java.util.Set<androidx.health.connect.client.permission.HealthPermission> permissions, kotlin.coroutines.Continuation<? super java.util.Set<? extends androidx.health.connect.client.permission.HealthPermission>>);
+    method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
+    method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract();
+    method public suspend Object? getGrantedPermissions(kotlin.coroutines.Continuation<? super java.util.Set<? extends java.lang.String>>);
     method public suspend Object? revokeAllPermissions(kotlin.coroutines.Continuation<? super kotlin.Unit>);
     field public static final androidx.health.connect.client.PermissionController.Companion Companion;
   }
 
   public static final class PermissionController.Companion {
-    method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract(optional String providerPackageName);
-    method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract();
+    method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
+    method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract();
   }
 
 }
@@ -102,14 +102,14 @@
 package androidx.health.connect.client.permission {
 
   public final class HealthPermission {
-    method public static androidx.health.connect.client.permission.HealthPermission createReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
-    method public static androidx.health.connect.client.permission.HealthPermission createWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
+    method public static String getReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
+    method public static String getWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
     field public static final androidx.health.connect.client.permission.HealthPermission.Companion Companion;
   }
 
   public static final class HealthPermission.Companion {
-    method public androidx.health.connect.client.permission.HealthPermission createReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
-    method public androidx.health.connect.client.permission.HealthPermission createWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
+    method public String getReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
+    method public String getWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
   }
 
 }
@@ -407,98 +407,6 @@
   public static final class ElevationGainedRecord.Companion {
   }
 
-  public final class ExerciseEventRecord implements androidx.health.connect.client.records.Record {
-    ctor public ExerciseEventRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int eventType, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public java.time.Instant getEndTime();
-    method public java.time.ZoneOffset? getEndZoneOffset();
-    method public int getEventType();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getStartTime();
-    method public java.time.ZoneOffset? getStartZoneOffset();
-    property public java.time.Instant endTime;
-    property public java.time.ZoneOffset? endZoneOffset;
-    property public final int eventType;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant startTime;
-    property public java.time.ZoneOffset? startZoneOffset;
-    field public static final androidx.health.connect.client.records.ExerciseEventRecord.Companion Companion;
-    field public static final int EVENT_TYPE_PAUSE = 1; // 0x1
-    field public static final int EVENT_TYPE_REST = 2; // 0x2
-    field public static final int EVENT_TYPE_UNKNOWN = 0; // 0x0
-  }
-
-  public static final class ExerciseEventRecord.Companion {
-  }
-
-  public final class ExerciseLapRecord implements androidx.health.connect.client.records.Record {
-    ctor public ExerciseLapRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.units.Length? length, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public java.time.Instant getEndTime();
-    method public java.time.ZoneOffset? getEndZoneOffset();
-    method public androidx.health.connect.client.units.Length? getLength();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getStartTime();
-    method public java.time.ZoneOffset? getStartZoneOffset();
-    property public java.time.Instant endTime;
-    property public java.time.ZoneOffset? endZoneOffset;
-    property public final androidx.health.connect.client.units.Length? length;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant startTime;
-    property public java.time.ZoneOffset? startZoneOffset;
-  }
-
-  public final class ExerciseRepetitionsRecord implements androidx.health.connect.client.records.Record {
-    ctor public ExerciseRepetitionsRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, long count, int type, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public long getCount();
-    method public java.time.Instant getEndTime();
-    method public java.time.ZoneOffset? getEndZoneOffset();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getStartTime();
-    method public java.time.ZoneOffset? getStartZoneOffset();
-    method public int getType();
-    property public final long count;
-    property public java.time.Instant endTime;
-    property public java.time.ZoneOffset? endZoneOffset;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant startTime;
-    property public java.time.ZoneOffset? startZoneOffset;
-    property public final int type;
-    field public static final androidx.health.connect.client.records.ExerciseRepetitionsRecord.Companion Companion;
-    field public static final int REPETITION_TYPE_ARM_CURL = 1; // 0x1
-    field public static final int REPETITION_TYPE_BACK_EXTENSION = 2; // 0x2
-    field public static final int REPETITION_TYPE_BALL_SLAM = 3; // 0x3
-    field public static final int REPETITION_TYPE_BENCH_PRESS = 4; // 0x4
-    field public static final int REPETITION_TYPE_BURPEE = 5; // 0x5
-    field public static final int REPETITION_TYPE_CRUNCH = 6; // 0x6
-    field public static final int REPETITION_TYPE_DEADLIFT = 7; // 0x7
-    field public static final int REPETITION_TYPE_DOUBLE_ARM_TRICEPS_EXTENSION = 8; // 0x8
-    field public static final int REPETITION_TYPE_DUMBBELL_ROW = 9; // 0x9
-    field public static final int REPETITION_TYPE_FRONT_RAISE = 10; // 0xa
-    field public static final int REPETITION_TYPE_HIP_THRUST = 11; // 0xb
-    field public static final int REPETITION_TYPE_HULA_HOOP = 12; // 0xc
-    field public static final int REPETITION_TYPE_JUMPING_JACK = 13; // 0xd
-    field public static final int REPETITION_TYPE_JUMP_ROPE = 14; // 0xe
-    field public static final int REPETITION_TYPE_KETTLEBELL_SWING = 15; // 0xf
-    field public static final int REPETITION_TYPE_LATERAL_RAISE = 16; // 0x10
-    field public static final int REPETITION_TYPE_LAT_PULL_DOWN = 17; // 0x11
-    field public static final int REPETITION_TYPE_LEG_CURL = 18; // 0x12
-    field public static final int REPETITION_TYPE_LEG_EXTENSION = 19; // 0x13
-    field public static final int REPETITION_TYPE_LEG_PRESS = 20; // 0x14
-    field public static final int REPETITION_TYPE_LEG_RAISE = 21; // 0x15
-    field public static final int REPETITION_TYPE_LUNGE = 22; // 0x16
-    field public static final int REPETITION_TYPE_MOUNTAIN_CLIMBER = 23; // 0x17
-    field public static final int REPETITION_TYPE_PLANK = 24; // 0x18
-    field public static final int REPETITION_TYPE_PULL_UP = 25; // 0x19
-    field public static final int REPETITION_TYPE_PUNCH = 26; // 0x1a
-    field public static final int REPETITION_TYPE_SHOULDER_PRESS = 27; // 0x1b
-    field public static final int REPETITION_TYPE_SINGLE_ARM_TRICEPS_EXTENSION = 28; // 0x1c
-    field public static final int REPETITION_TYPE_SIT_UP = 29; // 0x1d
-    field public static final int REPETITION_TYPE_SQUAT = 30; // 0x1e
-    field public static final int REPETITION_TYPE_UNKNOWN = 0; // 0x0
-  }
-
-  public static final class ExerciseRepetitionsRecord.Companion {
-  }
-
   public final class ExerciseSessionRecord implements androidx.health.connect.client.records.Record {
     ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
@@ -1176,34 +1084,6 @@
   public static final class StepsRecord.Companion {
   }
 
-  public final class SwimmingStrokesRecord implements androidx.health.connect.client.records.Record {
-    ctor public SwimmingStrokesRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int type, optional long count, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public long getCount();
-    method public java.time.Instant getEndTime();
-    method public java.time.ZoneOffset? getEndZoneOffset();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getStartTime();
-    method public java.time.ZoneOffset? getStartZoneOffset();
-    method public int getType();
-    property public final long count;
-    property public java.time.Instant endTime;
-    property public java.time.ZoneOffset? endZoneOffset;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant startTime;
-    property public java.time.ZoneOffset? startZoneOffset;
-    property public final int type;
-    field public static final androidx.health.connect.client.records.SwimmingStrokesRecord.Companion Companion;
-    field public static final int SWIMMING_TYPE_BACKSTROKE = 2; // 0x2
-    field public static final int SWIMMING_TYPE_BREASTSTROKE = 3; // 0x3
-    field public static final int SWIMMING_TYPE_BUTTERFLY = 4; // 0x4
-    field public static final int SWIMMING_TYPE_FREESTYLE = 1; // 0x1
-    field public static final int SWIMMING_TYPE_MIXED = 5; // 0x5
-    field public static final int SWIMMING_TYPE_OTHER = 0; // 0x0
-  }
-
-  public static final class SwimmingStrokesRecord.Companion {
-  }
-
   public final class TotalCaloriesBurnedRecord implements androidx.health.connect.client.records.Record {
     ctor public TotalCaloriesBurnedRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, androidx.health.connect.client.units.Energy energy, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 454854f..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,24 +24,24 @@
   }
 
   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);
   }
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface PermissionController {
-    method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract(optional String providerPackageName);
-    method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract();
-    method public suspend Object? getGrantedPermissions(java.util.Set<androidx.health.connect.client.permission.HealthPermission> permissions, kotlin.coroutines.Continuation<? super java.util.Set<? extends androidx.health.connect.client.permission.HealthPermission>>);
+    method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
+    method public default static androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract();
+    method public suspend Object? getGrantedPermissions(kotlin.coroutines.Continuation<? super java.util.Set<? extends java.lang.String>>);
     method public suspend Object? revokeAllPermissions(kotlin.coroutines.Continuation<? super kotlin.Unit>);
     field public static final androidx.health.connect.client.PermissionController.Companion Companion;
   }
 
   public static final class PermissionController.Companion {
-    method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract(optional String providerPackageName);
-    method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<androidx.health.connect.client.permission.HealthPermission>,java.util.Set<androidx.health.connect.client.permission.HealthPermission>> createRequestPermissionResultContract();
+    method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract(optional String providerPackageName);
+    method public androidx.activity.result.contract.ActivityResultContract<java.util.Set<java.lang.String>,java.util.Set<java.lang.String>> createRequestPermissionResultContract();
   }
 
 }
@@ -102,14 +102,14 @@
 package androidx.health.connect.client.permission {
 
   public final class HealthPermission {
-    method public static androidx.health.connect.client.permission.HealthPermission createReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
-    method public static androidx.health.connect.client.permission.HealthPermission createWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
+    method public static String getReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
+    method public static String getWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
     field public static final androidx.health.connect.client.permission.HealthPermission.Companion Companion;
   }
 
   public static final class HealthPermission.Companion {
-    method public androidx.health.connect.client.permission.HealthPermission createReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
-    method public androidx.health.connect.client.permission.HealthPermission createWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
+    method public String getReadPermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
+    method public String getWritePermission(kotlin.reflect.KClass<? extends androidx.health.connect.client.records.Record> recordType);
   }
 
 }
@@ -407,98 +407,6 @@
   public static final class ElevationGainedRecord.Companion {
   }
 
-  public final class ExerciseEventRecord implements androidx.health.connect.client.records.IntervalRecord {
-    ctor public ExerciseEventRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int eventType, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public java.time.Instant getEndTime();
-    method public java.time.ZoneOffset? getEndZoneOffset();
-    method public int getEventType();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getStartTime();
-    method public java.time.ZoneOffset? getStartZoneOffset();
-    property public java.time.Instant endTime;
-    property public java.time.ZoneOffset? endZoneOffset;
-    property public final int eventType;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant startTime;
-    property public java.time.ZoneOffset? startZoneOffset;
-    field public static final androidx.health.connect.client.records.ExerciseEventRecord.Companion Companion;
-    field public static final int EVENT_TYPE_PAUSE = 1; // 0x1
-    field public static final int EVENT_TYPE_REST = 2; // 0x2
-    field public static final int EVENT_TYPE_UNKNOWN = 0; // 0x0
-  }
-
-  public static final class ExerciseEventRecord.Companion {
-  }
-
-  public final class ExerciseLapRecord implements androidx.health.connect.client.records.IntervalRecord {
-    ctor public ExerciseLapRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.units.Length? length, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public java.time.Instant getEndTime();
-    method public java.time.ZoneOffset? getEndZoneOffset();
-    method public androidx.health.connect.client.units.Length? getLength();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getStartTime();
-    method public java.time.ZoneOffset? getStartZoneOffset();
-    property public java.time.Instant endTime;
-    property public java.time.ZoneOffset? endZoneOffset;
-    property public final androidx.health.connect.client.units.Length? length;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant startTime;
-    property public java.time.ZoneOffset? startZoneOffset;
-  }
-
-  public final class ExerciseRepetitionsRecord implements androidx.health.connect.client.records.IntervalRecord {
-    ctor public ExerciseRepetitionsRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, long count, int type, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public long getCount();
-    method public java.time.Instant getEndTime();
-    method public java.time.ZoneOffset? getEndZoneOffset();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getStartTime();
-    method public java.time.ZoneOffset? getStartZoneOffset();
-    method public int getType();
-    property public final long count;
-    property public java.time.Instant endTime;
-    property public java.time.ZoneOffset? endZoneOffset;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant startTime;
-    property public java.time.ZoneOffset? startZoneOffset;
-    property public final int type;
-    field public static final androidx.health.connect.client.records.ExerciseRepetitionsRecord.Companion Companion;
-    field public static final int REPETITION_TYPE_ARM_CURL = 1; // 0x1
-    field public static final int REPETITION_TYPE_BACK_EXTENSION = 2; // 0x2
-    field public static final int REPETITION_TYPE_BALL_SLAM = 3; // 0x3
-    field public static final int REPETITION_TYPE_BENCH_PRESS = 4; // 0x4
-    field public static final int REPETITION_TYPE_BURPEE = 5; // 0x5
-    field public static final int REPETITION_TYPE_CRUNCH = 6; // 0x6
-    field public static final int REPETITION_TYPE_DEADLIFT = 7; // 0x7
-    field public static final int REPETITION_TYPE_DOUBLE_ARM_TRICEPS_EXTENSION = 8; // 0x8
-    field public static final int REPETITION_TYPE_DUMBBELL_ROW = 9; // 0x9
-    field public static final int REPETITION_TYPE_FRONT_RAISE = 10; // 0xa
-    field public static final int REPETITION_TYPE_HIP_THRUST = 11; // 0xb
-    field public static final int REPETITION_TYPE_HULA_HOOP = 12; // 0xc
-    field public static final int REPETITION_TYPE_JUMPING_JACK = 13; // 0xd
-    field public static final int REPETITION_TYPE_JUMP_ROPE = 14; // 0xe
-    field public static final int REPETITION_TYPE_KETTLEBELL_SWING = 15; // 0xf
-    field public static final int REPETITION_TYPE_LATERAL_RAISE = 16; // 0x10
-    field public static final int REPETITION_TYPE_LAT_PULL_DOWN = 17; // 0x11
-    field public static final int REPETITION_TYPE_LEG_CURL = 18; // 0x12
-    field public static final int REPETITION_TYPE_LEG_EXTENSION = 19; // 0x13
-    field public static final int REPETITION_TYPE_LEG_PRESS = 20; // 0x14
-    field public static final int REPETITION_TYPE_LEG_RAISE = 21; // 0x15
-    field public static final int REPETITION_TYPE_LUNGE = 22; // 0x16
-    field public static final int REPETITION_TYPE_MOUNTAIN_CLIMBER = 23; // 0x17
-    field public static final int REPETITION_TYPE_PLANK = 24; // 0x18
-    field public static final int REPETITION_TYPE_PULL_UP = 25; // 0x19
-    field public static final int REPETITION_TYPE_PUNCH = 26; // 0x1a
-    field public static final int REPETITION_TYPE_SHOULDER_PRESS = 27; // 0x1b
-    field public static final int REPETITION_TYPE_SINGLE_ARM_TRICEPS_EXTENSION = 28; // 0x1c
-    field public static final int REPETITION_TYPE_SIT_UP = 29; // 0x1d
-    field public static final int REPETITION_TYPE_SQUAT = 30; // 0x1e
-    field public static final int REPETITION_TYPE_UNKNOWN = 0; // 0x0
-  }
-
-  public static final class ExerciseRepetitionsRecord.Companion {
-  }
-
   public final class ExerciseSessionRecord implements androidx.health.connect.client.records.IntervalRecord {
     ctor public ExerciseSessionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int exerciseType, optional String? title, optional String? notes, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
@@ -1199,34 +1107,6 @@
   public static final class StepsRecord.Companion {
   }
 
-  public final class SwimmingStrokesRecord implements androidx.health.connect.client.records.IntervalRecord {
-    ctor public SwimmingStrokesRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int type, optional long count, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public long getCount();
-    method public java.time.Instant getEndTime();
-    method public java.time.ZoneOffset? getEndZoneOffset();
-    method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.time.Instant getStartTime();
-    method public java.time.ZoneOffset? getStartZoneOffset();
-    method public int getType();
-    property public final long count;
-    property public java.time.Instant endTime;
-    property public java.time.ZoneOffset? endZoneOffset;
-    property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.time.Instant startTime;
-    property public java.time.ZoneOffset? startZoneOffset;
-    property public final int type;
-    field public static final androidx.health.connect.client.records.SwimmingStrokesRecord.Companion Companion;
-    field public static final int SWIMMING_TYPE_BACKSTROKE = 2; // 0x2
-    field public static final int SWIMMING_TYPE_BREASTSTROKE = 3; // 0x3
-    field public static final int SWIMMING_TYPE_BUTTERFLY = 4; // 0x4
-    field public static final int SWIMMING_TYPE_FREESTYLE = 1; // 0x1
-    field public static final int SWIMMING_TYPE_MIXED = 5; // 0x5
-    field public static final int SWIMMING_TYPE_OTHER = 0; // 0x0
-  }
-
-  public static final class SwimmingStrokesRecord.Companion {
-  }
-
   public final class TotalCaloriesBurnedRecord implements androidx.health.connect.client.records.IntervalRecord {
     ctor public TotalCaloriesBurnedRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, androidx.health.connect.client.units.Energy energy, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
diff --git a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/PermissionSamples.kt b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/PermissionSamples.kt
index e80bc61..b18cba92 100644
--- a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/PermissionSamples.kt
+++ b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/PermissionSamples.kt
@@ -29,16 +29,25 @@
     val requestPermission =
         activity.registerForActivityResult(
             PermissionController.createRequestPermissionResultContract()
-        ) { grantedPermissions ->
+        ) { grantedPermissions: Set<String> ->
             if (
-                grantedPermissions.contains(
-                    HealthPermission.createReadPermission(StepsRecord::class)
-                )
+                grantedPermissions.contains(HealthPermission.getReadPermission(StepsRecord::class))
             ) {
                 // Read or process steps related health records.
             } else {
                 // user denied permission
             }
         }
-    requestPermission.launch(setOf(HealthPermission.createReadPermission(StepsRecord::class)))
+    requestPermission.launch(setOf(HealthPermission.getReadPermission(StepsRecord::class)))
+}
+
+@Sampled
+suspend fun GetPermissions(permissionController: PermissionController) {
+    val grantedPermissions = permissionController.getGrantedPermissions()
+
+    if (grantedPermissions.contains(HealthPermission.getReadPermission(StepsRecord::class))) {
+        // Read or process steps related health records.
+    } else {
+        // user denied permission
+    }
 }
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/PermissionController.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
index e5aa0f7..c82bdd1 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
@@ -32,27 +32,25 @@
      *
      * @param permissions set of permissions interested to check if granted or not
      * @return set of granted permissions.
-     *
      * @throws android.os.RemoteException For any IPC transportation failures.
      * @throws java.io.IOException For any disk I/O issues.
      * @throws IllegalStateException If service is not available.
      */
-    suspend fun getGrantedPermissions(permissions: Set<HealthPermission>): Set<HealthPermission>
+    @RestrictTo(RestrictTo.Scope.LIBRARY) // To be deleted once internal clients have migrated.
+    suspend fun getGrantedPermissionsLegacy(
+        permissions: Set<HealthPermission>
+    ): Set<HealthPermission>
 
     /**
-     * Filters and returns a subset of permissions granted by the user to the calling app, out of
-     * the input permissions set.
+     * Returns a set of all health permissions granted by the user to the calling app.
      *
-     * @param permissions set of permissions to filter. Each permission should be one of the list
-     * defined in [HealthPermission]
-     * @return filtered set of granted permissions.
-     *
+     * @return set of granted permissions.
      * @throws android.os.RemoteException For any IPC transportation failures.
      * @throws java.io.IOException For any disk I/O issues.
      * @throws IllegalStateException If service is not available.
+     * @sample androidx.health.connect.client.samples.GetPermissions
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY) // Not yet ready for public
-    suspend fun filterGrantedPermissions(permissions: Set<String>): Set<String>
+    suspend fun getGrantedPermissions(): Set<String>
 
     /**
      * Revokes all previously granted [HealthPermission] by the user to the calling app.
@@ -68,14 +66,13 @@
          * Creates an [ActivityResultContract] to request Health permissions.
          *
          * @param providerPackageName Optional provider package name to request health permissions
-         * from.
-         *
+         *   from.
          * @see androidx.activity.ComponentActivity.registerForActivityResult
-         * @sample androidx.health.connect.client.samples.RequestPermission
          */
+        @RestrictTo(RestrictTo.Scope.LIBRARY) // To be deleted once internal clients have migrated.
         @JvmStatic
         @JvmOverloads
-        fun createRequestPermissionResultContract(
+        fun createRequestPermissionResultContractLegacy(
             providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME
         ): ActivityResultContract<Set<HealthPermission>, Set<HealthPermission>> {
             return HealthDataRequestPermissions(providerPackageName = providerPackageName)
@@ -85,15 +82,13 @@
          * Creates an [ActivityResultContract] to request Health permissions.
          *
          * @param providerPackageName Optional provider package name to request health permissions
-         * from.
-         *
-         * @see androidx.activity.ComponentActivity.registerForActivityResult
+         *   from.
          * @sample androidx.health.connect.client.samples.RequestPermission
+         * @see androidx.activity.ComponentActivity.registerForActivityResult
          */
         @JvmStatic
         @JvmOverloads
-        @RestrictTo(RestrictTo.Scope.LIBRARY) // Not yet ready for public
-        fun createRequestPermissionResultContractInternal(
+        fun createRequestPermissionResultContract(
             providerPackageName: String = DEFAULT_PROVIDER_PACKAGE_NAME
         ): ActivityResultContract<Set<String>, Set<String>> {
             return HealthDataRequestPermissionsInternal(providerPackageName = providerPackageName)
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 cd206a5..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,11 +64,17 @@
  */
 class HealthConnectClientImpl
 internal constructor(
-    private val providerPackageName: String,
     private val delegate: HealthDataAsyncClient,
+    private val allPermissions: List<String> =
+        HealthPermission.RECORD_TYPE_TO_PERMISSION.flatMap {
+            listOf<String>(
+                HealthPermission.WRITE_PERMISSION_PREFIX + it.value,
+                HealthPermission.READ_PERMISSION_PREFIX + it.value
+            )
+        },
 ) : HealthConnectClient, PermissionController {
 
-    override suspend fun getGrantedPermissions(
+    override suspend fun getGrantedPermissionsLegacy(
         permissions: Set<HealthPermission>
     ): Set<HealthPermission> {
         val grantedPermissions =
@@ -84,21 +90,20 @@
         return grantedPermissions
     }
 
-    override suspend fun filterGrantedPermissions(
-        permissions: Set<String>
-    ): Set<String> {
+    override suspend fun getGrantedPermissions(): Set<String> {
         val grantedPermissions =
             delegate
                 .filterGrantedPermissions(
-                    permissions.map {
-                        PermissionProto.Permission.newBuilder().setPermission(it).build() }
-                        .toSet())
+                    allPermissions
+                        .map { PermissionProto.Permission.newBuilder().setPermission(it).build() }
+                        .toSet()
+                )
                 .await()
                 .map { it.permission }
                 .toSet()
         Logger.debug(
             HEALTH_CONNECT_CLIENT_TAG,
-            "Granted ${grantedPermissions.size} out of ${permissions.size} permissions."
+            "Granted ${grantedPermissions.size} out of ${allPermissions.size} permissions."
         )
         return grantedPermissions
     }
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/datatype/RecordsTypeNameMap.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/datatype/RecordsTypeNameMap.kt
index 6b9cbd8..547c584 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/datatype/RecordsTypeNameMap.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/datatype/RecordsTypeNameMap.kt
@@ -31,23 +31,11 @@
 import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
 import androidx.health.connect.client.records.DistanceRecord
 import androidx.health.connect.client.records.ElevationGainedRecord
-import androidx.health.connect.client.records.ExerciseEventRecord
-import androidx.health.connect.client.records.ExerciseLapRecord
-import androidx.health.connect.client.records.ExerciseRepetitionsRecord
 import androidx.health.connect.client.records.ExerciseSessionRecord
 import androidx.health.connect.client.records.FloorsClimbedRecord
 import androidx.health.connect.client.records.HeartRateRecord
-import androidx.health.connect.client.records.HeartRateVariabilityDifferentialIndexRecord
 import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySd2Record
-import androidx.health.connect.client.records.HeartRateVariabilitySdannRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySdnnIndexRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySdnnRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySdsdRecord
-import androidx.health.connect.client.records.HeartRateVariabilityTinnRecord
 import androidx.health.connect.client.records.HeightRecord
-import androidx.health.connect.client.records.HipCircumferenceRecord
 import androidx.health.connect.client.records.HydrationRecord
 import androidx.health.connect.client.records.IntermenstrualBleedingRecord
 import androidx.health.connect.client.records.LeanBodyMassRecord
@@ -66,10 +54,8 @@
 import androidx.health.connect.client.records.SpeedRecord
 import androidx.health.connect.client.records.StepsCadenceRecord
 import androidx.health.connect.client.records.StepsRecord
-import androidx.health.connect.client.records.SwimmingStrokesRecord
 import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
 import androidx.health.connect.client.records.Vo2MaxRecord
-import androidx.health.connect.client.records.WaistCircumferenceRecord
 import androidx.health.connect.client.records.WeightRecord
 import androidx.health.connect.client.records.WheelchairPushesRecord
 import kotlin.reflect.KClass
@@ -77,8 +63,6 @@
 val RECORDS_TYPE_NAME_MAP: Map<String, KClass<out Record>> =
     mapOf(
         "ActiveCaloriesBurned" to ActiveCaloriesBurnedRecord::class,
-        "ActivityEvent" to ExerciseEventRecord::class, // Keep legacy Activity name
-        "ActivityLap" to ExerciseLapRecord::class, // Keep legacy Activity name
         "ActivitySession" to ExerciseSessionRecord::class, // Keep legacy Activity name
         "BasalBodyTemperature" to BasalBodyTemperatureRecord::class,
         "BasalMetabolicRate" to BasalMetabolicRateRecord::class,
@@ -95,18 +79,8 @@
         "ElevationGained" to ElevationGainedRecord::class,
         "FloorsClimbed" to FloorsClimbedRecord::class,
         "HeartRateSeries" to HeartRateRecord::class, // Keep legacy Series suffix
-        "HeartRateVariabilityDifferentialIndex" to
-            HeartRateVariabilityDifferentialIndexRecord::class,
         "HeartRateVariabilityRmssd" to HeartRateVariabilityRmssdRecord::class,
-        "HeartRateVariabilityS" to HeartRateVariabilitySRecord::class,
-        "HeartRateVariabilitySd2" to HeartRateVariabilitySd2Record::class,
-        "HeartRateVariabilitySdann" to HeartRateVariabilitySdannRecord::class,
-        "HeartRateVariabilitySdnn" to HeartRateVariabilitySdnnRecord::class,
-        "HeartRateVariabilitySdnnIndex" to HeartRateVariabilitySdnnIndexRecord::class,
-        "HeartRateVariabilitySdsd" to HeartRateVariabilitySdsdRecord::class,
-        "HeartRateVariabilityTinn" to HeartRateVariabilityTinnRecord::class,
         "Height" to HeightRecord::class,
-        "HipCircumference" to HipCircumferenceRecord::class,
         "Hydration" to HydrationRecord::class,
         "LeanBodyMass" to LeanBodyMassRecord::class,
         "Menstruation" to MenstruationFlowRecord::class,
@@ -115,7 +89,6 @@
         "OvulationTest" to OvulationTestRecord::class,
         "OxygenSaturation" to OxygenSaturationRecord::class,
         "PowerSeries" to PowerRecord::class, // Keep legacy Series suffix
-        "Repetitions" to ExerciseRepetitionsRecord::class, // Keep legacy Repetitions name
         "RespiratoryRate" to RespiratoryRateRecord::class,
         "RestingHeartRate" to RestingHeartRateRecord::class,
         "SexualActivity" to SexualActivityRecord::class,
@@ -125,10 +98,8 @@
         "IntermenstrualBleeding" to IntermenstrualBleedingRecord::class,
         "Steps" to StepsRecord::class,
         "StepsCadenceSeries" to StepsCadenceRecord::class, // Keep legacy Series suffix
-        "SwimmingStrokes" to SwimmingStrokesRecord::class,
         "TotalCaloriesBurned" to TotalCaloriesBurnedRecord::class,
         "Vo2Max" to Vo2MaxRecord::class,
-        "WaistCircumference" to WaistCircumferenceRecord::class,
         "WheelchairPushes" to WheelchairPushesRecord::class,
         "Weight" to WeightRecord::class,
     )
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
index 29b8a3e..31e6a1c 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
@@ -34,23 +34,11 @@
 import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
 import androidx.health.connect.client.records.DistanceRecord
 import androidx.health.connect.client.records.ElevationGainedRecord
-import androidx.health.connect.client.records.ExerciseEventRecord
-import androidx.health.connect.client.records.ExerciseLapRecord
-import androidx.health.connect.client.records.ExerciseRepetitionsRecord
 import androidx.health.connect.client.records.ExerciseSessionRecord
 import androidx.health.connect.client.records.FloorsClimbedRecord
 import androidx.health.connect.client.records.HeartRateRecord
-import androidx.health.connect.client.records.HeartRateVariabilityDifferentialIndexRecord
 import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySd2Record
-import androidx.health.connect.client.records.HeartRateVariabilitySdannRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySdnnIndexRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySdnnRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySdsdRecord
-import androidx.health.connect.client.records.HeartRateVariabilityTinnRecord
 import androidx.health.connect.client.records.HeightRecord
-import androidx.health.connect.client.records.HipCircumferenceRecord
 import androidx.health.connect.client.records.HydrationRecord
 import androidx.health.connect.client.records.IntermenstrualBleedingRecord
 import androidx.health.connect.client.records.LeanBodyMassRecord
@@ -70,10 +58,8 @@
 import androidx.health.connect.client.records.SpeedRecord
 import androidx.health.connect.client.records.StepsCadenceRecord
 import androidx.health.connect.client.records.StepsRecord
-import androidx.health.connect.client.records.SwimmingStrokesRecord
 import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
 import androidx.health.connect.client.records.Vo2MaxRecord
-import androidx.health.connect.client.records.WaistCircumferenceRecord
 import androidx.health.connect.client.records.WeightRecord
 import androidx.health.connect.client.records.WheelchairPushesRecord
 import androidx.health.connect.client.units.BloodGlucose
@@ -251,20 +237,6 @@
                     zoneOffset = zoneOffset,
                     metadata = metadata
                 )
-            "HipCircumference" ->
-                HipCircumferenceRecord(
-                    circumference = getDouble("circumference").meters,
-                    time = time,
-                    zoneOffset = zoneOffset,
-                    metadata = metadata
-                )
-            "HeartRateVariabilityDifferentialIndex" ->
-                HeartRateVariabilityDifferentialIndexRecord(
-                    heartRateVariabilityMillis = getDouble("heartRateVariability"),
-                    time = time,
-                    zoneOffset = zoneOffset,
-                    metadata = metadata
-                )
             "HeartRateVariabilityRmssd" ->
                 HeartRateVariabilityRmssdRecord(
                     heartRateVariabilityMillis = getDouble("heartRateVariability"),
@@ -272,55 +244,6 @@
                     zoneOffset = zoneOffset,
                     metadata = metadata
                 )
-            "HeartRateVariabilityS" ->
-                HeartRateVariabilitySRecord(
-                    heartRateVariabilityMillis = getDouble("heartRateVariability"),
-                    time = time,
-                    zoneOffset = zoneOffset,
-                    metadata = metadata
-                )
-            "HeartRateVariabilitySd2" ->
-                HeartRateVariabilitySd2Record(
-                    heartRateVariabilityMillis = getDouble("heartRateVariability"),
-                    time = time,
-                    zoneOffset = zoneOffset,
-                    metadata = metadata
-                )
-            "HeartRateVariabilitySdann" ->
-                HeartRateVariabilitySdannRecord(
-                    heartRateVariabilityMillis = getDouble("heartRateVariability"),
-                    time = time,
-                    zoneOffset = zoneOffset,
-                    metadata = metadata
-                )
-            "HeartRateVariabilitySdnnIndex" ->
-                HeartRateVariabilitySdnnIndexRecord(
-                    heartRateVariabilityMillis = getDouble("heartRateVariability"),
-                    time = time,
-                    zoneOffset = zoneOffset,
-                    metadata = metadata
-                )
-            "HeartRateVariabilitySdnn" ->
-                HeartRateVariabilitySdnnRecord(
-                    heartRateVariabilityMillis = getDouble("heartRateVariability"),
-                    time = time,
-                    zoneOffset = zoneOffset,
-                    metadata = metadata
-                )
-            "HeartRateVariabilitySdsd" ->
-                HeartRateVariabilitySdsdRecord(
-                    heartRateVariabilityMillis = getDouble("heartRateVariability"),
-                    time = time,
-                    zoneOffset = zoneOffset,
-                    metadata = metadata
-                )
-            "HeartRateVariabilityTinn" ->
-                HeartRateVariabilityTinnRecord(
-                    heartRateVariabilityMillis = getDouble("heartRateVariability"),
-                    time = time,
-                    zoneOffset = zoneOffset,
-                    metadata = metadata
-                )
             "LeanBodyMass" ->
                 LeanBodyMassRecord(
                     mass = getDouble("mass").kilograms,
@@ -451,13 +374,6 @@
                     zoneOffset = zoneOffset,
                     metadata = metadata
                 )
-            "WaistCircumference" ->
-                WaistCircumferenceRecord(
-                    circumference = getDouble("circumference").meters,
-                    time = time,
-                    zoneOffset = zoneOffset,
-                    metadata = metadata
-                )
             "Weight" ->
                 WeightRecord(
                     weight = getDouble("weight").kilograms,
@@ -474,29 +390,6 @@
                     endZoneOffset = endZoneOffset,
                     metadata = metadata
                 )
-            "ActivityEvent" ->
-                ExerciseEventRecord(
-                    eventType =
-                        mapEnum(
-                            "eventType",
-                            ExerciseEventRecord.EVENT_TYPE_STRING_TO_INT_MAP,
-                            ExerciseEventRecord.EVENT_TYPE_UNKNOWN,
-                        ),
-                    startTime = startTime,
-                    startZoneOffset = startZoneOffset,
-                    endTime = endTime,
-                    endZoneOffset = endZoneOffset,
-                    metadata = metadata
-                )
-            "ActivityLap" ->
-                ExerciseLapRecord(
-                    length = getDouble("length").meters,
-                    startTime = startTime,
-                    startZoneOffset = startZoneOffset,
-                    endTime = endTime,
-                    endZoneOffset = endZoneOffset,
-                    metadata = metadata
-                )
             "ActivitySession" ->
                 ExerciseSessionRecord(
                     exerciseType =
@@ -606,21 +499,6 @@
                     endZoneOffset = endZoneOffset,
                     metadata = metadata
                 )
-            "Repetitions" ->
-                ExerciseRepetitionsRecord(
-                    count = getLong("count"),
-                    type =
-                        mapEnum(
-                            "type",
-                            ExerciseRepetitionsRecord.REPETITION_TYPE_STRING_TO_INT_MAP,
-                            ExerciseRepetitionsRecord.REPETITION_TYPE_UNKNOWN
-                        ),
-                    startTime = startTime,
-                    startZoneOffset = startZoneOffset,
-                    endTime = endTime,
-                    endZoneOffset = endZoneOffset,
-                    metadata = metadata
-                )
             "SleepSession" ->
                 SleepSessionRecord(
                     title = getString("title"),
@@ -660,21 +538,6 @@
                     endZoneOffset = endZoneOffset,
                     metadata = metadata
                 )
-            "SwimmingStrokes" ->
-                SwimmingStrokesRecord(
-                    count = getLong("count"),
-                    type =
-                        mapEnum(
-                            "type",
-                            SwimmingStrokesRecord.SWIMMING_TYPE_STRING_TO_INT_MAP,
-                            SwimmingStrokesRecord.SWIMMING_TYPE_OTHER
-                        ),
-                    startTime = startTime,
-                    startZoneOffset = startZoneOffset,
-                    endTime = endTime,
-                    endZoneOffset = endZoneOffset,
-                    metadata = metadata
-                )
             "TotalCaloriesBurned" ->
                 TotalCaloriesBurnedRecord(
                     energy = getDouble("energy").kilocalories,
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
index 306673f..f87edca 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
@@ -34,23 +34,11 @@
 import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
 import androidx.health.connect.client.records.DistanceRecord
 import androidx.health.connect.client.records.ElevationGainedRecord
-import androidx.health.connect.client.records.ExerciseEventRecord
-import androidx.health.connect.client.records.ExerciseLapRecord
-import androidx.health.connect.client.records.ExerciseRepetitionsRecord
 import androidx.health.connect.client.records.ExerciseSessionRecord
 import androidx.health.connect.client.records.FloorsClimbedRecord
 import androidx.health.connect.client.records.HeartRateRecord
-import androidx.health.connect.client.records.HeartRateVariabilityDifferentialIndexRecord
 import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySd2Record
-import androidx.health.connect.client.records.HeartRateVariabilitySdannRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySdnnIndexRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySdnnRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySdsdRecord
-import androidx.health.connect.client.records.HeartRateVariabilityTinnRecord
 import androidx.health.connect.client.records.HeightRecord
-import androidx.health.connect.client.records.HipCircumferenceRecord
 import androidx.health.connect.client.records.HydrationRecord
 import androidx.health.connect.client.records.IntermenstrualBleedingRecord
 import androidx.health.connect.client.records.LeanBodyMassRecord
@@ -72,10 +60,8 @@
 import androidx.health.connect.client.records.SpeedRecord
 import androidx.health.connect.client.records.StepsCadenceRecord
 import androidx.health.connect.client.records.StepsRecord
-import androidx.health.connect.client.records.SwimmingStrokesRecord
 import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
 import androidx.health.connect.client.records.Vo2MaxRecord
-import androidx.health.connect.client.records.WaistCircumferenceRecord
 import androidx.health.connect.client.records.WeightRecord
 import androidx.health.connect.client.records.WheelchairPushesRecord
 import androidx.health.platform.client.proto.DataProto
@@ -198,56 +184,11 @@
                 .setDataType(protoDataType("Height"))
                 .apply { putValues("height", doubleVal(height.inMeters)) }
                 .build()
-        is HipCircumferenceRecord ->
-            instantaneousProto()
-                .setDataType(protoDataType("HipCircumference"))
-                .apply { putValues("circumference", doubleVal(circumference.inMeters)) }
-                .build()
-        is HeartRateVariabilityDifferentialIndexRecord ->
-            instantaneousProto()
-                .setDataType(protoDataType("HeartRateVariabilityDifferentialIndex"))
-                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
-                .build()
         is HeartRateVariabilityRmssdRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("HeartRateVariabilityRmssd"))
                 .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                 .build()
-        is HeartRateVariabilitySRecord ->
-            instantaneousProto()
-                .setDataType(protoDataType("HeartRateVariabilityS"))
-                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
-                .build()
-        is HeartRateVariabilitySd2Record ->
-            instantaneousProto()
-                .setDataType(protoDataType("HeartRateVariabilitySd2"))
-                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
-                .build()
-        is HeartRateVariabilitySdannRecord ->
-            instantaneousProto()
-                .setDataType(protoDataType("HeartRateVariabilitySdann"))
-                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
-                .build()
-        is HeartRateVariabilitySdnnIndexRecord ->
-            instantaneousProto()
-                .setDataType(protoDataType("HeartRateVariabilitySdnnIndex"))
-                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
-                .build()
-        is HeartRateVariabilitySdnnRecord ->
-            instantaneousProto()
-                .setDataType(protoDataType("HeartRateVariabilitySdnn"))
-                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
-                .build()
-        is HeartRateVariabilitySdsdRecord ->
-            instantaneousProto()
-                .setDataType(protoDataType("HeartRateVariabilitySdsd"))
-                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
-                .build()
-        is HeartRateVariabilityTinnRecord ->
-            instantaneousProto()
-                .setDataType(protoDataType("HeartRateVariabilityTinn"))
-                .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
-                .build()
         is IntermenstrualBleedingRecord ->
             instantaneousProto().setDataType(protoDataType("IntermenstrualBleeding")).build()
         is LeanBodyMassRecord ->
@@ -265,9 +206,7 @@
                 }
                 .build()
         is MenstruationPeriodRecord ->
-            intervalProto()
-                .setDataType(protoDataType("MenstruationPeriod"))
-                .build()
+            intervalProto().setDataType(protoDataType("MenstruationPeriod")).build()
         is OvulationTestRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("OvulationTest"))
@@ -336,11 +275,6 @@
                         ?.let { putValues("measurementMethod", it) }
                 }
                 .build()
-        is WaistCircumferenceRecord ->
-            instantaneousProto()
-                .setDataType(protoDataType("WaistCircumference"))
-                .apply { putValues("circumference", doubleVal(circumference.inMeters)) }
-                .build()
         is WeightRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("Weight"))
@@ -351,23 +285,6 @@
                 .setDataType(protoDataType("ActiveCaloriesBurned"))
                 .apply { putValues("energy", doubleVal(energy.inKilocalories)) }
                 .build()
-        is ExerciseEventRecord ->
-            intervalProto()
-                .setDataType(protoDataType("ActivityEvent"))
-                .apply {
-                    enumValFromInt(eventType, ExerciseEventRecord.EVENT_TYPE_INT_TO_STRING_MAP)
-                        ?.let { putValues("eventType", it) }
-                }
-                .build()
-        is ExerciseLapRecord ->
-            intervalProto()
-                .setDataType(protoDataType("ActivityLap"))
-                .apply {
-                    if (length != null) {
-                        putValues("length", doubleVal(length.inMeters))
-                    }
-                }
-                .build()
         is ExerciseSessionRecord ->
             intervalProto()
                 .setDataType(protoDataType("ActivitySession"))
@@ -539,18 +456,6 @@
                     name?.let { putValues("name", stringVal(it)) }
                 }
                 .build()
-        is ExerciseRepetitionsRecord ->
-            intervalProto()
-                .setDataType(protoDataType("Repetitions"))
-                .apply {
-                    putValues("count", longVal(count))
-                    enumValFromInt(
-                            type,
-                            ExerciseRepetitionsRecord.REPETITION_TYPE_INT_TO_STRING_MAP
-                        )
-                        ?.let { putValues("type", it) }
-                }
-                .build()
         is SleepSessionRecord ->
             intervalProto()
                 .setDataType(protoDataType("SleepSession"))
@@ -573,19 +478,6 @@
                 .setDataType(protoDataType("Steps"))
                 .apply { putValues("count", longVal(count)) }
                 .build()
-        is SwimmingStrokesRecord ->
-            intervalProto()
-                .setDataType(protoDataType("SwimmingStrokes"))
-                .apply {
-                    if (count > 0) {
-                        putValues("count", longVal(count))
-                    }
-                    val swimmingType =
-                        enumValFromInt(type, SwimmingStrokesRecord.SWIMMING_TYPE_INT_TO_STRING_MAP)
-                            ?: enumVal(SwimmingStrokesRecord.SwimmingType.OTHER)
-                    putValues("type", swimmingType)
-                }
-                .build()
         is TotalCaloriesBurnedRecord ->
             intervalProto()
                 .setDataType(protoDataType("TotalCaloriesBurned"))
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
index b1f93ed..463f45f 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
@@ -29,23 +29,11 @@
 import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
 import androidx.health.connect.client.records.DistanceRecord
 import androidx.health.connect.client.records.ElevationGainedRecord
-import androidx.health.connect.client.records.ExerciseEventRecord
-import androidx.health.connect.client.records.ExerciseLapRecord
-import androidx.health.connect.client.records.ExerciseRepetitionsRecord
 import androidx.health.connect.client.records.ExerciseSessionRecord
 import androidx.health.connect.client.records.FloorsClimbedRecord
 import androidx.health.connect.client.records.HeartRateRecord
-import androidx.health.connect.client.records.HeartRateVariabilityDifferentialIndexRecord
 import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySd2Record
-import androidx.health.connect.client.records.HeartRateVariabilitySdannRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySdnnIndexRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySdnnRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySdsdRecord
-import androidx.health.connect.client.records.HeartRateVariabilityTinnRecord
 import androidx.health.connect.client.records.HeightRecord
-import androidx.health.connect.client.records.HipCircumferenceRecord
 import androidx.health.connect.client.records.HydrationRecord
 import androidx.health.connect.client.records.IntermenstrualBleedingRecord
 import androidx.health.connect.client.records.LeanBodyMassRecord
@@ -64,10 +52,8 @@
 import androidx.health.connect.client.records.SpeedRecord
 import androidx.health.connect.client.records.StepsCadenceRecord
 import androidx.health.connect.client.records.StepsRecord
-import androidx.health.connect.client.records.SwimmingStrokesRecord
 import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
 import androidx.health.connect.client.records.Vo2MaxRecord
-import androidx.health.connect.client.records.WaistCircumferenceRecord
 import androidx.health.connect.client.records.WeightRecord
 import androidx.health.connect.client.records.WheelchairPushesRecord
 import kotlin.reflect.KClass
@@ -89,10 +75,11 @@
          * Creates [HealthPermission] to read provided [recordType], such as `Steps::class`.
          *
          * @return Permission object to use with
-         * [androidx.health.connect.client.PermissionController].
+         *   [androidx.health.connect.client.PermissionController].
          */
+        @RestrictTo(RestrictTo.Scope.LIBRARY) // To be deleted.
         @JvmStatic
-        public fun createReadPermission(recordType: KClass<out Record>): HealthPermission {
+        public fun createReadPermissionLegacy(recordType: KClass<out Record>): HealthPermission {
             return HealthPermission(recordType, AccessTypes.READ)
         }
 
@@ -103,9 +90,8 @@
          * @return Permission to use with [androidx.health.connect.client.PermissionController].
          * @throws IllegalArgumentException if the given record type is invalid.
          */
-        @RestrictTo(RestrictTo.Scope.LIBRARY) // Not yet ready for public
         @JvmStatic
-        public fun createReadPermissionInternal(recordType: KClass<out Record>): String {
+        public fun getReadPermission(recordType: KClass<out Record>): String {
             if (RECORD_TYPE_TO_PERMISSION[recordType] == null) {
                 throw IllegalArgumentException(
                     "Given recordType is not valid : $recordType.simpleName"
@@ -119,8 +105,9 @@
          *
          * @return Permission to use with [androidx.health.connect.client.PermissionController].
          */
+        @RestrictTo(RestrictTo.Scope.LIBRARY) // To be deleted.
         @JvmStatic
-        public fun createWritePermission(recordType: KClass<out Record>): HealthPermission {
+        public fun createWritePermissionLegacy(recordType: KClass<out Record>): HealthPermission {
             return HealthPermission(recordType, AccessTypes.WRITE)
         }
 
@@ -129,12 +116,11 @@
          * `Steps::class`.
          *
          * @return Permission object to use with
-         * [androidx.health.connect.client.PermissionController].
+         *   [androidx.health.connect.client.PermissionController].
          * @throws IllegalArgumentException if the given record type is invalid.
          */
-        @RestrictTo(RestrictTo.Scope.LIBRARY) // Not yet ready for public
         @JvmStatic
-        public fun createWritePermissionInternal(recordType: KClass<out Record>): String {
+        public fun getWritePermission(recordType: KClass<out Record>): String {
             if (RECORD_TYPE_TO_PERMISSION[recordType] == null) {
                 throw IllegalArgumentException(
                     "Given recordType is not valid : $recordType.simpleName"
@@ -144,193 +130,138 @@
         }
 
         // Read permissions for ACTIVITY.
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_ACTIVE_CALORIES_BURNED =
+        internal const val READ_ACTIVE_CALORIES_BURNED =
             "android.permission.health.READ_ACTIVE_CALORIES_BURNED"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_DISTANCE = "android.permission.health.READ_DISTANCE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_ELEVATION_GAINED = "android.permission.health.READ_ELEVATION_GAINED"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_EXERCISE = "android.permission.health.READ_EXERCISE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_FLOORS_CLIMBED = "android.permission.health.READ_FLOORS_CLIMBED"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_STEPS = "android.permission.health.READ_STEPS"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_TOTAL_CALORIES_BURNED =
+        internal const val READ_DISTANCE = "android.permission.health.READ_DISTANCE"
+        internal const val READ_ELEVATION_GAINED = "android.permission.health.READ_ELEVATION_GAINED"
+        internal const val READ_EXERCISE = "android.permission.health.READ_EXERCISE"
+        internal const val READ_FLOORS_CLIMBED = "android.permission.health.READ_FLOORS_CLIMBED"
+        internal const val READ_STEPS = "android.permission.health.READ_STEPS"
+        internal const val READ_TOTAL_CALORIES_BURNED =
             "android.permission.health.READ_TOTAL_CALORIES_BURNED"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_VO2_MAX = "android.permission.health.READ_VO2_MAX"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_WHEELCHAIR_PUSHES = "android.permission.health.READ_WHEELCHAIR_PUSHES"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_POWER = "android.permission.health.READ_POWER"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_SPEED = "android.permission.health.READ_SPEED"
+        internal const val READ_VO2_MAX = "android.permission.health.READ_VO2_MAX"
+        internal const val READ_WHEELCHAIR_PUSHES =
+            "android.permission.health.READ_WHEELCHAIR_PUSHES"
+        internal const val READ_POWER = "android.permission.health.READ_POWER"
+        internal const val READ_SPEED = "android.permission.health.READ_SPEED"
 
         // Read permissions for BODY_MEASUREMENTS.
+        internal const val READ_BASAL_METABOLIC_RATE =
+            "android.permission.health.READ_BASAL_METABOLIC_RATE"
+        internal const val READ_BODY_FAT = "android.permission.health.READ_BODY_FAT"
+        internal const val READ_BODY_WATER_MASS = "android.permission.health.READ_BODY_WATER_MASS"
+        internal const val READ_BONE_MASS = "android.permission.health.READ_BONE_MASS"
+        internal const val READ_HEIGHT = "android.permission.health.READ_HEIGHT"
         @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_BASAL_METABOLIC_RATE = "android.permission.health.READ_BASAL_METABOLIC_RATE"
+        internal const val READ_HIP_CIRCUMFERENCE =
+            "android.permission.health.READ_HIP_CIRCUMFERENCE"
+        internal const val READ_LEAN_BODY_MASS = "android.permission.health.READ_LEAN_BODY_MASS"
         @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_BODY_FAT = "android.permission.health.READ_BODY_FAT"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_BODY_WATER_MASS = "android.permission.health.READ_BODY_WATER_MASS"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_BONE_MASS = "android.permission.health.READ_BONE_MASS"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_HEIGHT = "android.permission.health.READ_HEIGHT"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_HIP_CIRCUMFERENCE = "android.permission.health.READ_HIP_CIRCUMFERENCE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_LEAN_BODY_MASS = "android.permission.health.READ_LEAN_BODY_MASS"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_WAIST_CIRCUMFERENCE = "android.permission.health.READ_WAIST_CIRCUMFERENCE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_WEIGHT = "android.permission.health.READ_WEIGHT"
+        internal const val READ_WAIST_CIRCUMFERENCE =
+            "android.permission.health.READ_WAIST_CIRCUMFERENCE"
+        internal const val READ_WEIGHT = "android.permission.health.READ_WEIGHT"
 
         // Read permissions for CYCLE_TRACKING.
+        internal const val READ_CERVICAL_MUCUS = "android.permission.health.READ_CERVICAL_MUCUS"
         @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_CERVICAL_MUCUS = "android.permission.health.READ_CERVICAL_MUCUS"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_MENSTRUATION = "android.permission.health.READ_MENSTRUATION"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_OVULATION_TEST = "android.permission.health.READ_OVULATION_TEST"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_SEXUAL_ACTIVITY = "android.permission.health.READ_SEXUAL_ACTIVITY"
+        internal const val READ_INTERMENSTRUAL_BLEEDING =
+            "android.permission.health.READ_INTERMENSTRUAL_BLEEDING"
+        internal const val READ_MENSTRUATION = "android.permission.health.READ_MENSTRUATION"
+        internal const val READ_OVULATION_TEST = "android.permission.health.READ_OVULATION_TEST"
+        internal const val READ_SEXUAL_ACTIVITY = "android.permission.health.READ_SEXUAL_ACTIVITY"
 
         // Read permissions for NUTRITION.
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_HYDRATION = "android.permission.health.READ_HYDRATION"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_NUTRITION = "android.permission.health.READ_NUTRITION"
+        internal const val READ_HYDRATION = "android.permission.health.READ_HYDRATION"
+        internal const val READ_NUTRITION = "android.permission.health.READ_NUTRITION"
 
         // Read permissions for SLEEP.
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_SLEEP = "android.permission.health.READ_SLEEP"
+        internal const val READ_SLEEP = "android.permission.health.READ_SLEEP"
 
         // Read permissions for VITALS.
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_BASAL_BODY_TEMPERATURE =
+        internal const val READ_BASAL_BODY_TEMPERATURE =
             "android.permission.health.READ_BASAL_BODY_TEMPERATURE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_BLOOD_GLUCOSE = "android.permission.health.READ_BLOOD_GLUCOSE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_BLOOD_PRESSURE = "android.permission.health.READ_BLOOD_PRESSURE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_BODY_TEMPERATURE = "android.permission.health.READ_BODY_TEMPERATURE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_HEART_RATE = "android.permission.health.READ_HEART_RATE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_HEART_RATE_VARIABILITY =
+        internal const val READ_BLOOD_GLUCOSE = "android.permission.health.READ_BLOOD_GLUCOSE"
+        internal const val READ_BLOOD_PRESSURE = "android.permission.health.READ_BLOOD_PRESSURE"
+        internal const val READ_BODY_TEMPERATURE = "android.permission.health.READ_BODY_TEMPERATURE"
+        internal const val READ_HEART_RATE = "android.permission.health.READ_HEART_RATE"
+        internal const val READ_HEART_RATE_VARIABILITY =
             "android.permission.health.READ_HEART_RATE_VARIABILITY"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_INTERMENSTRUAL_BLEEDING =
-            "android.permission.health.READ_INTERMENSTRUAL_BLEEDING"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_OXYGEN_SATURATION = "android.permission.health.READ_OXYGEN_SATURATION"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_RESPIRATORY_RATE = "android.permission.health.READ_RESPIRATORY_RATE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val READ_RESTING_HEART_RATE = "android.permission.health.READ_RESTING_HEART_RATE"
+        internal const val READ_OXYGEN_SATURATION =
+            "android.permission.health.READ_OXYGEN_SATURATION"
+        internal const val READ_RESPIRATORY_RATE = "android.permission.health.READ_RESPIRATORY_RATE"
+        internal const val READ_RESTING_HEART_RATE =
+            "android.permission.health.READ_RESTING_HEART_RATE"
 
         // Write permissions for ACTIVITY.
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_ACTIVE_CALORIES_BURNED =
+        internal const val WRITE_ACTIVE_CALORIES_BURNED =
             "android.permission.health.WRITE_ACTIVE_CALORIES_BURNED"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_DISTANCE = "android.permission.health.WRITE_DISTANCE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_ELEVATION_GAINED = "android.permission.health.WRITE_ELEVATION_GAINED"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_EXERCISE = "android.permission.health.WRITE_EXERCISE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_FLOORS_CLIMBED = "android.permission.health.WRITE_FLOORS_CLIMBED"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_STEPS = "android.permission.health.WRITE_STEPS"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_TOTAL_CALORIES_BURNED =
+        internal const val WRITE_DISTANCE = "android.permission.health.WRITE_DISTANCE"
+        internal const val WRITE_ELEVATION_GAINED =
+            "android.permission.health.WRITE_ELEVATION_GAINED"
+        internal const val WRITE_EXERCISE = "android.permission.health.WRITE_EXERCISE"
+        internal const val WRITE_FLOORS_CLIMBED = "android.permission.health.WRITE_FLOORS_CLIMBED"
+        internal const val WRITE_STEPS = "android.permission.health.WRITE_STEPS"
+        internal const val WRITE_TOTAL_CALORIES_BURNED =
             "android.permission.health.WRITE_TOTAL_CALORIES_BURNED"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_VO2_MAX = "android.permission.health.WRITE_VO2_MAX"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_WHEELCHAIR_PUSHES = "android.permission.health.WRITE_WHEELCHAIR_PUSHES"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_POWER = "android.permission.health.WRITE_POWER"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_SPEED = "android.permission.health.WRITE_SPEED"
+        internal const val WRITE_VO2_MAX = "android.permission.health.WRITE_VO2_MAX"
+        internal const val WRITE_WHEELCHAIR_PUSHES =
+            "android.permission.health.WRITE_WHEELCHAIR_PUSHES"
+        internal const val WRITE_POWER = "android.permission.health.WRITE_POWER"
+        internal const val WRITE_SPEED = "android.permission.health.WRITE_SPEED"
 
         // Write permissions for BODY_MEASUREMENTS.
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_BASAL_METABOLIC_RATE =
+        internal const val WRITE_BASAL_METABOLIC_RATE =
             "android.permission.health.WRITE_BASAL_METABOLIC_RATE"
+        internal const val WRITE_BODY_FAT = "android.permission.health.WRITE_BODY_FAT"
+        internal const val WRITE_BODY_WATER_MASS = "android.permission.health.WRITE_BODY_WATER_MASS"
+        internal const val WRITE_BONE_MASS = "android.permission.health.WRITE_BONE_MASS"
+        internal const val WRITE_HEIGHT = "android.permission.health.WRITE_HEIGHT"
         @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_BODY_FAT = "android.permission.health.WRITE_BODY_FAT"
+        internal const val WRITE_HIP_CIRCUMFERENCE =
+            "android.permission.health.WRITE_HIP_CIRCUMFERENCE"
+        internal const val WRITE_LEAN_BODY_MASS = "android.permission.health.WRITE_LEAN_BODY_MASS"
         @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_BODY_WATER_MASS = "android.permission.health.WRITE_BODY_WATER_MASS"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_BONE_MASS = "android.permission.health.WRITE_BONE_MASS"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_HEIGHT = "android.permission.health.WRITE_HEIGHT"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_HIP_CIRCUMFERENCE = "android.permission.health.WRITE_HIP_CIRCUMFERENCE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_INTERMENSTRUAL_BLEEDING =
-            "android.permission.health.WRITE_INTERMENSTRUAL_BLEEDING"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_LEAN_BODY_MASS = "android.permission.health.WRITE_LEAN_BODY_MASS"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_WAIST_CIRCUMFERENCE = "android.permission.health.WRITE_WAIST_CIRCUMFERENCE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_WEIGHT = "android.permission.health.WRITE_WEIGHT"
+        internal const val WRITE_WAIST_CIRCUMFERENCE =
+            "android.permission.health.WRITE_WAIST_CIRCUMFERENCE"
+        internal const val WRITE_WEIGHT = "android.permission.health.WRITE_WEIGHT"
 
         // Write permissions for CYCLE_TRACKING.
+        internal const val WRITE_CERVICAL_MUCUS = "android.permission.health.WRITE_CERVICAL_MUCUS"
         @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_CERVICAL_MUCUS = "android.permission.health.WRITE_CERVICAL_MUCUS"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_MENSTRUATION = "android.permission.health.WRITE_MENSTRUATION"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_OVULATION_TEST = "android.permission.health.WRITE_OVULATION_TEST"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_SEXUAL_ACTIVITY = "android.permission.health.WRITE_SEXUAL_ACTIVITY"
+        internal const val WRITE_INTERMENSTRUAL_BLEEDING =
+            "android.permission.health.WRITE_INTERMENSTRUAL_BLEEDING"
+        internal const val WRITE_MENSTRUATION = "android.permission.health.WRITE_MENSTRUATION"
+        internal const val WRITE_OVULATION_TEST = "android.permission.health.WRITE_OVULATION_TEST"
+        internal const val WRITE_SEXUAL_ACTIVITY = "android.permission.health.WRITE_SEXUAL_ACTIVITY"
 
         // Write permissions for NUTRITION.
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_HYDRATION = "android.permission.health.WRITE_HYDRATION"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_NUTRITION = "android.permission.health.WRITE_NUTRITION"
+        internal const val WRITE_HYDRATION = "android.permission.health.WRITE_HYDRATION"
+        internal const val WRITE_NUTRITION = "android.permission.health.WRITE_NUTRITION"
 
         // Write permissions for SLEEP.
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_SLEEP = "android.permission.health.WRITE_SLEEP"
+        internal const val WRITE_SLEEP = "android.permission.health.WRITE_SLEEP"
 
         // Write permissions for VITALS.
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_BASAL_BODY_TEMPERATURE =
+        internal const val WRITE_BASAL_BODY_TEMPERATURE =
             "android.permission.health.WRITE_BASAL_BODY_TEMPERATURE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_BLOOD_GLUCOSE = "android.permission.health.WRITE_BLOOD_GLUCOSE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_BLOOD_PRESSURE = "android.permission.health.WRITE_BLOOD_PRESSURE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_BODY_TEMPERATURE = "android.permission.health.WRITE_BODY_TEMPERATURE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_HEART_RATE = "android.permission.health.WRITE_HEART_RATE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_HEART_RATE_VARIABILITY =
+        internal const val WRITE_BLOOD_GLUCOSE = "android.permission.health.WRITE_BLOOD_GLUCOSE"
+        internal const val WRITE_BLOOD_PRESSURE = "android.permission.health.WRITE_BLOOD_PRESSURE"
+        internal const val WRITE_BODY_TEMPERATURE =
+            "android.permission.health.WRITE_BODY_TEMPERATURE"
+        internal const val WRITE_HEART_RATE = "android.permission.health.WRITE_HEART_RATE"
+        internal const val WRITE_HEART_RATE_VARIABILITY =
             "android.permission.health.WRITE_HEART_RATE_VARIABILITY"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_OXYGEN_SATURATION = "android.permission.health.WRITE_OXYGEN_SATURATION"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_RESPIRATORY_RATE = "android.permission.health.WRITE_RESPIRATORY_RATE"
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        const val WRITE_RESTING_HEART_RATE = "android.permission.health.WRITE_RESTING_HEART_RATE"
+        internal const val WRITE_OXYGEN_SATURATION =
+            "android.permission.health.WRITE_OXYGEN_SATURATION"
+        internal const val WRITE_RESPIRATORY_RATE =
+            "android.permission.health.WRITE_RESPIRATORY_RATE"
+        internal const val WRITE_RESTING_HEART_RATE =
+            "android.permission.health.WRITE_RESTING_HEART_RATE"
 
-        private const val READ_PERMISSION_PREFIX = "android.permission.health.READ_"
+        internal const val READ_PERMISSION_PREFIX = "android.permission.health.READ_"
+        internal const val WRITE_PERMISSION_PREFIX = "android.permission.health.WRITE_"
 
-        private const val WRITE_PERMISSION_PREFIX = "android.permission.health.WRITE_"
-
-        private val RECORD_TYPE_TO_PERMISSION =
+        internal val RECORD_TYPE_TO_PERMISSION =
             mapOf<KClass<out Record>, String>(
                 ActiveCaloriesBurnedRecord::class to
                     READ_ACTIVE_CALORIES_BURNED.substringAfter(READ_PERMISSION_PREFIX),
@@ -355,36 +286,14 @@
                 DistanceRecord::class to READ_DISTANCE.substringAfter(READ_PERMISSION_PREFIX),
                 ElevationGainedRecord::class to
                     READ_ELEVATION_GAINED.substringAfter(READ_PERMISSION_PREFIX),
-                ExerciseEventRecord::class to READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
-                ExerciseLapRecord::class to READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
-                ExerciseRepetitionsRecord::class to
-                    READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
                 ExerciseSessionRecord::class to
                     READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
                 FloorsClimbedRecord::class to
                     READ_FLOORS_CLIMBED.substringAfter(READ_PERMISSION_PREFIX),
                 HeartRateRecord::class to READ_HEART_RATE.substringAfter(READ_PERMISSION_PREFIX),
-                HeartRateVariabilityDifferentialIndexRecord::class to
-                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
                 HeartRateVariabilityRmssdRecord::class to
                     READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
-                HeartRateVariabilitySd2Record::class to
-                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
-                HeartRateVariabilitySdannRecord::class to
-                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
-                HeartRateVariabilitySdnnIndexRecord::class to
-                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
-                HeartRateVariabilitySdnnRecord::class to
-                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
-                HeartRateVariabilitySdsdRecord::class to
-                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
-                HeartRateVariabilitySRecord::class to
-                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
-                HeartRateVariabilityTinnRecord::class to
-                    READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
                 HeightRecord::class to READ_HEIGHT.substringAfter(READ_PERMISSION_PREFIX),
-                HipCircumferenceRecord::class to
-                    READ_HIP_CIRCUMFERENCE.substringAfter(READ_PERMISSION_PREFIX),
                 HydrationRecord::class to READ_HYDRATION.substringAfter(READ_PERMISSION_PREFIX),
                 IntermenstrualBleedingRecord::class to
                     READ_INTERMENSTRUAL_BLEEDING.substringAfter(READ_PERMISSION_PREFIX),
@@ -411,13 +320,9 @@
                 SpeedRecord::class to READ_SPEED.substringAfter(READ_PERMISSION_PREFIX),
                 StepsCadenceRecord::class to READ_STEPS.substringAfter(READ_PERMISSION_PREFIX),
                 StepsRecord::class to READ_STEPS.substringAfter(READ_PERMISSION_PREFIX),
-                SwimmingStrokesRecord::class to
-                    READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
                 TotalCaloriesBurnedRecord::class to
                     READ_TOTAL_CALORIES_BURNED.substringAfter(READ_PERMISSION_PREFIX),
                 Vo2MaxRecord::class to READ_VO2_MAX.substringAfter(READ_PERMISSION_PREFIX),
-                WaistCircumferenceRecord::class to
-                    READ_WAIST_CIRCUMFERENCE.substringAfter(READ_PERMISSION_PREFIX),
                 WeightRecord::class to READ_WEIGHT.substringAfter(READ_PERMISSION_PREFIX),
                 WheelchairPushesRecord::class to
                     READ_WHEELCHAIR_PUSHES.substringAfter(READ_PERMISSION_PREFIX),
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseEventRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseEventRecord.kt
deleted file mode 100644
index 3bb5153..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseEventRecord.kt
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.IntDef
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.records.metadata.Metadata
-import java.time.Instant
-import java.time.ZoneOffset
-
-/**
- * Captures pause or rest events within an exercise. Each record contains the start / stop time of
- * the event.
- *
- * For pause events, resume state can be assumed from the end time of the pause or rest event.
- */
-public class ExerciseEventRecord(
-    override val startTime: Instant,
-    override val startZoneOffset: ZoneOffset?,
-    override val endTime: Instant,
-    override val endZoneOffset: ZoneOffset?,
-    /**
-     * Type of event. Required field. Allowed values: [EventType].
-     *
-     * @see EventType
-     */
-    @property:EventTypes public val eventType: Int,
-    override val metadata: Metadata = Metadata.EMPTY,
-) : IntervalRecord {
-
-    init {
-        require(startTime.isBefore(endTime)) { "startTime must be before endTime." }
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is ExerciseEventRecord) return false
-
-        if (eventType != other.eventType) return false
-        if (startTime != other.startTime) return false
-        if (startZoneOffset != other.startZoneOffset) return false
-        if (endTime != other.endTime) return false
-        if (endZoneOffset != other.endZoneOffset) return false
-        if (metadata != other.metadata) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = eventType.hashCode()
-        result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
-        result = 31 * result + endTime.hashCode()
-        result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
-        result = 31 * result + metadata.hashCode()
-        return result
-    }
-
-    /**
-     * Types of exercise event. They can be either explicitly requested by a user or auto-detected
-     * by a tracking app.
-     */
-    internal object EventType {
-        /**
-         * Explicit pause during a workout, requested by the user (by clicking a pause button in
-         * the session UI). Movement happening during pause should not contribute to session
-         * metrics.
-         */
-        const val PAUSE = "pause"
-        /**
-         * Auto-detected periods of rest during a workout. There should be no user movement
-         * detected during rest and any movement detected should finish rest event.
-         */
-        const val REST = "rest"
-    }
-    /**
-     * Types of exercise event. They can be either explicitly requested by a user or auto-detected
-     * by a tracking app.
-     *
-     * @suppress
-     */
-    @Retention(AnnotationRetention.SOURCE)
-    @IntDef(
-        value =
-            [
-                EVENT_TYPE_UNKNOWN,
-                EVENT_TYPE_PAUSE,
-                EVENT_TYPE_REST,
-            ]
-    )
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    annotation class EventTypes
-
-    companion object {
-        const val EVENT_TYPE_UNKNOWN = 0
-        const val EVENT_TYPE_PAUSE = 1
-        const val EVENT_TYPE_REST = 2
-
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        @JvmField
-        val EVENT_TYPE_STRING_TO_INT_MAP: Map<String, Int> =
-            mapOf(
-                EventType.PAUSE to EVENT_TYPE_PAUSE,
-                EventType.REST to EVENT_TYPE_REST,
-            )
-
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        @JvmField
-        val EVENT_TYPE_INT_TO_STRING_MAP = EVENT_TYPE_STRING_TO_INT_MAP.reverse()
-    }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseLapRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseLapRecord.kt
deleted file mode 100644
index 883db30..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseLapRecord.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.records
-
-import androidx.health.connect.client.records.metadata.Metadata
-import androidx.health.connect.client.units.Length
-import androidx.health.connect.client.units.meters
-import java.time.Instant
-import java.time.ZoneOffset
-
-/**
- * Captures the time of a lap within an exercise. A lap is explicitly marked segment within an
- * exercise session (such as pool length while swimming or a track lap while running). Each record
- * contains the start / stop time of the lap.
- */
-public class ExerciseLapRecord(
-    override val startTime: Instant,
-    override val startZoneOffset: ZoneOffset?,
-    override val endTime: Instant,
-    override val endZoneOffset: ZoneOffset?,
-    /** Length of the lap, in meters. Optional field. Valid range: 0-1000000 meters. */
-    public val length: Length? = null,
-    override val metadata: Metadata = Metadata.EMPTY,
-) : IntervalRecord {
-
-    init {
-        length?.requireNotLess(other = length.zero(), name = "length")
-        length?.requireNotMore(other = MAX_LAP_LENGTH, name = "length")
-        require(startTime.isBefore(endTime)) { "startTime must be before endTime." }
-    }
-
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
-     */
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is ExerciseLapRecord) return false
-
-        if (length != other.length) return false
-        if (startTime != other.startTime) return false
-        if (startZoneOffset != other.startZoneOffset) return false
-        if (endTime != other.endTime) return false
-        if (endZoneOffset != other.endZoneOffset) return false
-        if (metadata != other.metadata) return false
-
-        return true
-    }
-
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
-     */
-    override fun hashCode(): Int {
-        var result = length?.hashCode() ?: 0
-        result = 31 * result + startTime.hashCode()
-        result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
-        result = 31 * result + endTime.hashCode()
-        result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
-        result = 31 * result + metadata.hashCode()
-        return result
-    }
-
-    private companion object {
-        private val MAX_LAP_LENGTH = 1000_000.meters
-    }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRepetitionsRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRepetitionsRecord.kt
deleted file mode 100644
index f29b0d1..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRepetitionsRecord.kt
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.IntDef
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.records.metadata.Metadata
-import java.time.Instant
-import java.time.ZoneOffset
-
-/** Captures the number of repetitions in an exercise set. */
-public class ExerciseRepetitionsRecord(
-    override val startTime: Instant,
-    override val startZoneOffset: ZoneOffset?,
-    override val endTime: Instant,
-    override val endZoneOffset: ZoneOffset?,
-    /** Count. Required field. Valid range: 1-1000000. */
-    public val count: Long,
-    /** Type of exercise being repeated. Required field. */
-    @property:RepetitionTypes public val type: Int,
-    override val metadata: Metadata = Metadata.EMPTY,
-) : IntervalRecord {
-
-    init {
-        requireNonNegative(value = count, name = "count")
-        count.requireNotMore(other = 1000_000, name = "count")
-        require(startTime.isBefore(endTime)) { "startTime must be before endTime." }
-    }
-
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is ExerciseRepetitionsRecord) return false
-
-        if (count != other.count) return false
-        if (type != other.type) return false
-        if (startTime != other.startTime) return false
-        if (startZoneOffset != other.startZoneOffset) return false
-        if (endTime != other.endTime) return false
-        if (endZoneOffset != other.endZoneOffset) return false
-        if (metadata != other.metadata) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = count.hashCode()
-        result = 31 * result + type.hashCode()
-        result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
-        result = 31 * result + endTime.hashCode()
-        result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
-        result = 31 * result + metadata.hashCode()
-        return result
-    }
-
-    companion object {
-        const val REPETITION_TYPE_UNKNOWN = 0
-        const val REPETITION_TYPE_ARM_CURL = 1
-        const val REPETITION_TYPE_BACK_EXTENSION = 2
-        const val REPETITION_TYPE_BALL_SLAM = 3
-        const val REPETITION_TYPE_BENCH_PRESS = 4
-        const val REPETITION_TYPE_BURPEE = 5
-        const val REPETITION_TYPE_CRUNCH = 6
-        const val REPETITION_TYPE_DEADLIFT = 7
-        const val REPETITION_TYPE_DOUBLE_ARM_TRICEPS_EXTENSION = 8
-        const val REPETITION_TYPE_DUMBBELL_ROW = 9
-        const val REPETITION_TYPE_FRONT_RAISE = 10
-        const val REPETITION_TYPE_HIP_THRUST = 11
-        const val REPETITION_TYPE_HULA_HOOP = 12
-        const val REPETITION_TYPE_JUMPING_JACK = 13
-        const val REPETITION_TYPE_JUMP_ROPE = 14
-        const val REPETITION_TYPE_KETTLEBELL_SWING = 15
-        const val REPETITION_TYPE_LATERAL_RAISE = 16
-        const val REPETITION_TYPE_LAT_PULL_DOWN = 17
-        const val REPETITION_TYPE_LEG_CURL = 18
-        const val REPETITION_TYPE_LEG_EXTENSION = 19
-        const val REPETITION_TYPE_LEG_PRESS = 20
-        const val REPETITION_TYPE_LEG_RAISE = 21
-        const val REPETITION_TYPE_LUNGE = 22
-        const val REPETITION_TYPE_MOUNTAIN_CLIMBER = 23
-        const val REPETITION_TYPE_PLANK = 24
-        const val REPETITION_TYPE_PULL_UP = 25
-        const val REPETITION_TYPE_PUNCH = 26
-        const val REPETITION_TYPE_SHOULDER_PRESS = 27
-        const val REPETITION_TYPE_SINGLE_ARM_TRICEPS_EXTENSION = 28
-        const val REPETITION_TYPE_SIT_UP = 29
-        const val REPETITION_TYPE_SQUAT = 30
-
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        @JvmField
-        val REPETITION_TYPE_STRING_TO_INT_MAP: Map<String, Int> =
-            mapOf(
-                "arm_curl" to REPETITION_TYPE_ARM_CURL,
-                "back_extension" to REPETITION_TYPE_BACK_EXTENSION,
-                "ball_slam" to REPETITION_TYPE_BALL_SLAM,
-                "bench_press" to REPETITION_TYPE_BENCH_PRESS,
-                "burpee" to REPETITION_TYPE_BURPEE,
-                "crunch" to REPETITION_TYPE_CRUNCH,
-                "deadlift" to REPETITION_TYPE_DEADLIFT,
-                "double_arm_triceps_extension" to REPETITION_TYPE_DOUBLE_ARM_TRICEPS_EXTENSION,
-                "dumbbell_row" to REPETITION_TYPE_DUMBBELL_ROW,
-                "front_raise" to REPETITION_TYPE_FRONT_RAISE,
-                "hip_thrust" to REPETITION_TYPE_HIP_THRUST,
-                "hula_hoop" to REPETITION_TYPE_HULA_HOOP,
-                "jumping_jack" to REPETITION_TYPE_JUMPING_JACK,
-                "jump_rope" to REPETITION_TYPE_JUMP_ROPE,
-                "kettlebell_swing" to REPETITION_TYPE_KETTLEBELL_SWING,
-                "lateral_raise" to REPETITION_TYPE_LATERAL_RAISE,
-                "lat_pull_down" to REPETITION_TYPE_LAT_PULL_DOWN,
-                "leg_curl" to REPETITION_TYPE_LEG_CURL,
-                "leg_extension" to REPETITION_TYPE_LEG_EXTENSION,
-                "leg_press" to REPETITION_TYPE_LEG_PRESS,
-                "leg_raise" to REPETITION_TYPE_LEG_RAISE,
-                "lunge" to REPETITION_TYPE_LUNGE,
-                "mountain_climber" to REPETITION_TYPE_MOUNTAIN_CLIMBER,
-                "plank" to REPETITION_TYPE_PLANK,
-                "pull_up" to REPETITION_TYPE_PULL_UP,
-                "punch" to REPETITION_TYPE_PUNCH,
-                "shoulder_press" to REPETITION_TYPE_SHOULDER_PRESS,
-                "single_arm_triceps_extension" to REPETITION_TYPE_SINGLE_ARM_TRICEPS_EXTENSION,
-                "sit_up" to REPETITION_TYPE_SIT_UP,
-                "squat" to REPETITION_TYPE_SQUAT,
-            )
-
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        @JvmField
-        val REPETITION_TYPE_INT_TO_STRING_MAP =
-            REPETITION_TYPE_STRING_TO_INT_MAP.entries.associateBy({ it.value }, { it.key })
-    }
-
-    /**
-     * Exercise types supported by repetitions.
-     * @suppress
-     */
-    @Retention(AnnotationRetention.SOURCE)
-    @IntDef(
-        value =
-            [
-                REPETITION_TYPE_ARM_CURL,
-                REPETITION_TYPE_BACK_EXTENSION,
-                REPETITION_TYPE_BALL_SLAM,
-                REPETITION_TYPE_BENCH_PRESS,
-                REPETITION_TYPE_BURPEE,
-                REPETITION_TYPE_CRUNCH,
-                REPETITION_TYPE_DEADLIFT,
-                REPETITION_TYPE_DOUBLE_ARM_TRICEPS_EXTENSION,
-                REPETITION_TYPE_DUMBBELL_ROW,
-                REPETITION_TYPE_FRONT_RAISE,
-                REPETITION_TYPE_HIP_THRUST,
-                REPETITION_TYPE_HULA_HOOP,
-                REPETITION_TYPE_JUMPING_JACK,
-                REPETITION_TYPE_JUMP_ROPE,
-                REPETITION_TYPE_KETTLEBELL_SWING,
-                REPETITION_TYPE_LATERAL_RAISE,
-                REPETITION_TYPE_LAT_PULL_DOWN,
-                REPETITION_TYPE_LEG_CURL,
-                REPETITION_TYPE_LEG_EXTENSION,
-                REPETITION_TYPE_LEG_PRESS,
-                REPETITION_TYPE_LEG_RAISE,
-                REPETITION_TYPE_LUNGE,
-                REPETITION_TYPE_MOUNTAIN_CLIMBER,
-                REPETITION_TYPE_PLANK,
-                REPETITION_TYPE_PULL_UP,
-                REPETITION_TYPE_PUNCH,
-                REPETITION_TYPE_SHOULDER_PRESS,
-                REPETITION_TYPE_SINGLE_ARM_TRICEPS_EXTENSION,
-                REPETITION_TYPE_SIT_UP,
-                REPETITION_TYPE_SQUAT,
-            ]
-    )
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    annotation class RepetitionTypes
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRouteRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRouteRecord.kt
deleted file mode 100644
index c32e4ee..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseRouteRecord.kt
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.FloatRange
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.records.metadata.Metadata
-import androidx.health.connect.client.units.Length
-import java.time.Instant
-import java.time.ZoneOffset
-
-/**
- * Captures the user's route during exercise, e.g. during running or cycling. Each record represents
- * a series of location points.
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class ExerciseRouteRecord(
-  override val startTime: Instant,
-  override val startZoneOffset: ZoneOffset?,
-  override val endTime: Instant,
-  override val endZoneOffset: ZoneOffset?,
-  override val samples: List<Location>,
-  override val metadata: Metadata = Metadata.EMPTY,
-) : SeriesRecord<ExerciseRouteRecord.Location> {
-
-  init {
-    require(!startTime.isAfter(endTime)) { "startTime must not be after endTime." }
-  }
-
-  override fun equals(other: Any?): Boolean {
-    if (this === other) return true
-    if (other !is ExerciseRouteRecord) return false
-
-    if (startTime != other.startTime) return false
-    if (startZoneOffset != other.startZoneOffset) return false
-    if (endTime != other.endTime) return false
-    if (endZoneOffset != other.endZoneOffset) return false
-    if (samples != other.samples) return false
-    if (metadata != other.metadata) return false
-
-    return true
-  }
-
-  override fun hashCode(): Int {
-    var result = startTime.hashCode()
-    result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
-    result = 31 * result + endTime.hashCode()
-    result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
-    result = 31 * result + samples.hashCode()
-    result = 31 * result + metadata.hashCode()
-    return result
-  }
-
-  /**
-   * Represents a single location in an exercise route.
-   *
-   * @param time The point in time when the measurement was taken.
-   * @param latitude Latitude of a location represented as a float, in degrees. Valid
-   * range: -180 - 180 degrees.
-   * @param longitude Longitude of a location represented as a float, in degrees. Valid
-   * range: -180 - 180 degrees.
-   * @param horizontalAccuracy The radius of uncertainty for the location, in [Length] unit.
-   * @param altitude An altitude of a location represented as a float, in [Length] unit above sea
-   * level.
-   * @param verticalAccuracy The validity of the altitude values, and their estimated uncertainty,
-   * in [Length] unit.
-   *
-   * @see ExerciseRouteRecord
-   */
-  public class Location(
-    val time: Instant,
-    @FloatRange(from = MIN_COORDINATE, to = MAX_COORDINATE) val latitude: Float,
-    @FloatRange(from = MIN_COORDINATE, to = MAX_COORDINATE) val longitude: Float,
-    val horizontalAccuracy: Length? = null,
-    val altitude: Length? = null,
-    val verticalAccuracy: Length? = null
-  ) {
-
-    override fun equals(other: Any?): Boolean {
-      if (this === other) return true
-      if (other !is Location) return false
-
-      if (time != other.time) return false
-      if (latitude != other.latitude) return false
-      if (longitude != other.longitude) return false
-      if (horizontalAccuracy != other.horizontalAccuracy) return false
-      if (altitude != other.altitude) return false
-      if (verticalAccuracy != other.verticalAccuracy) return false
-
-      return true
-    }
-
-    override fun hashCode(): Int {
-
-      var result = time.hashCode()
-      result = 31 * result + latitude.hashCode()
-      result = 31 * result + longitude.hashCode()
-      result = 31 * result + horizontalAccuracy.hashCode()
-      result = 31 * result + altitude.hashCode()
-      result = 31 * result + verticalAccuracy.hashCode()
-      return result
-    }
-  }
-
-  private companion object {
-    private const val MIN_COORDINATE = -180.0
-    private const val MAX_COORDINATE = 180.0
-  }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilityDifferentialIndexRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilityDifferentialIndexRecord.kt
deleted file mode 100644
index 3ca9306..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilityDifferentialIndexRecord.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.records.metadata.Metadata
-import java.time.Instant
-import java.time.ZoneOffset
-
-/**
- * Captures user's heart rate variability (HRV) measured as the difference between the widths of the
- * histogram of differences between adjacent heart beats measured at selected heights.
- *
- * @suppress
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class HeartRateVariabilityDifferentialIndexRecord(
-    override val time: Instant,
-    override val zoneOffset: ZoneOffset?,
-    /** Heart rate variability in milliseconds. Required field. */
-    public val heartRateVariabilityMillis: Double,
-    override val metadata: Metadata = Metadata.EMPTY,
-) : InstantaneousRecord {
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is HeartRateVariabilityDifferentialIndexRecord) return false
-
-        if (heartRateVariabilityMillis != other.heartRateVariabilityMillis) return false
-        if (time != other.time) return false
-        if (zoneOffset != other.zoneOffset) return false
-        if (metadata != other.metadata) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + heartRateVariabilityMillis.hashCode()
-        result = 31 * result + time.hashCode()
-        result = 31 * result + (zoneOffset?.hashCode() ?: 0)
-        result = 31 * result + metadata.hashCode()
-        return result
-    }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySRecord.kt
deleted file mode 100644
index 168870a..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySRecord.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.records.metadata.Metadata
-import java.time.Instant
-import java.time.ZoneOffset
-
-/**
- * Captures user's heart rate variability (HRV) as measured by the area of the elipse on a Poincare
- * plot.
- *
- * @suppress
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class HeartRateVariabilitySRecord(
-    override val time: Instant,
-    override val zoneOffset: ZoneOffset?,
-    /** Heart rate variability in milliseconds. Required field. */
-    public val heartRateVariabilityMillis: Double,
-    override val metadata: Metadata = Metadata.EMPTY,
-) : InstantaneousRecord {
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is HeartRateVariabilitySRecord) return false
-
-        if (heartRateVariabilityMillis != other.heartRateVariabilityMillis) return false
-        if (time != other.time) return false
-        if (zoneOffset != other.zoneOffset) return false
-        if (metadata != other.metadata) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + heartRateVariabilityMillis.hashCode()
-        result = 31 * result + time.hashCode()
-        result = 31 * result + (zoneOffset?.hashCode() ?: 0)
-        result = 31 * result + metadata.hashCode()
-        return result
-    }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySd2Record.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySd2Record.kt
deleted file mode 100644
index 6e19d5d..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySd2Record.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.records.metadata.Metadata
-import java.time.Instant
-import java.time.ZoneOffset
-
-/**
- * Captures user's heart rate variability (HRV) as measured by the Poincaré plot standard deviation
- * along the line of identity.
- *
- * @suppress
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class HeartRateVariabilitySd2Record(
-    override val time: Instant,
-    override val zoneOffset: ZoneOffset?,
-    /** Heart rate variability in milliseconds. Required field. */
-    public val heartRateVariabilityMillis: Double,
-    override val metadata: Metadata = Metadata.EMPTY,
-) : InstantaneousRecord {
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is HeartRateVariabilitySd2Record) return false
-
-        if (heartRateVariabilityMillis != other.heartRateVariabilityMillis) return false
-        if (time != other.time) return false
-        if (zoneOffset != other.zoneOffset) return false
-        if (metadata != other.metadata) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + heartRateVariabilityMillis.hashCode()
-        result = 31 * result + time.hashCode()
-        result = 31 * result + (zoneOffset?.hashCode() ?: 0)
-        result = 31 * result + metadata.hashCode()
-        return result
-    }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySdannRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySdannRecord.kt
deleted file mode 100644
index 6d959c1..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySdannRecord.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.records.metadata.Metadata
-import java.time.Instant
-import java.time.ZoneOffset
-
-/**
- * Captures user's heart rate variability (HRV) as measured by the standard deviation of the
- * averages of NN intervals in all 5-minute segments of the entire recording.
- *
- * @suppress
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class HeartRateVariabilitySdannRecord(
-    override val time: Instant,
-    override val zoneOffset: ZoneOffset?,
-    /** Heart rate variability in milliseconds. Required field. */
-    public val heartRateVariabilityMillis: Double,
-    override val metadata: Metadata = Metadata.EMPTY,
-) : InstantaneousRecord {
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is HeartRateVariabilitySdannRecord) return false
-
-        if (heartRateVariabilityMillis != other.heartRateVariabilityMillis) return false
-        if (time != other.time) return false
-        if (zoneOffset != other.zoneOffset) return false
-        if (metadata != other.metadata) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + heartRateVariabilityMillis.hashCode()
-        result = 31 * result + time.hashCode()
-        result = 31 * result + (zoneOffset?.hashCode() ?: 0)
-        result = 31 * result + metadata.hashCode()
-        return result
-    }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySdnnIndexRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySdnnIndexRecord.kt
deleted file mode 100644
index c607428..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySdnnIndexRecord.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.records.metadata.Metadata
-import java.time.Instant
-import java.time.ZoneOffset
-
-/**
- * Captures user's heart rate variability (HRV) as measured by the mean of the standard deviations
- * of all NN intervals for all of the recording.
- *
- * @suppress
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class HeartRateVariabilitySdnnIndexRecord(
-    override val time: Instant,
-    override val zoneOffset: ZoneOffset?,
-    /** Heart rate variability in milliseconds. Required field. */
-    public val heartRateVariabilityMillis: Double,
-    override val metadata: Metadata = Metadata.EMPTY,
-) : InstantaneousRecord {
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is HeartRateVariabilitySdnnIndexRecord) return false
-
-        if (heartRateVariabilityMillis != other.heartRateVariabilityMillis) return false
-        if (time != other.time) return false
-        if (zoneOffset != other.zoneOffset) return false
-        if (metadata != other.metadata) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + heartRateVariabilityMillis.hashCode()
-        result = 31 * result + time.hashCode()
-        result = 31 * result + (zoneOffset?.hashCode() ?: 0)
-        result = 31 * result + metadata.hashCode()
-        return result
-    }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySdnnRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySdnnRecord.kt
deleted file mode 100644
index 39732cf..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySdnnRecord.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.records.metadata.Metadata
-import java.time.Instant
-import java.time.ZoneOffset
-
-/**
- * Captures user's heart rate variability (HRV) as measured by the standard deviation of all N-N
- * intervals.
- *
- * @suppress
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class HeartRateVariabilitySdnnRecord(
-    override val time: Instant,
-    override val zoneOffset: ZoneOffset?,
-    /** Heart rate variability in milliseconds. Required field. */
-    public val heartRateVariabilityMillis: Double,
-    override val metadata: Metadata = Metadata.EMPTY,
-) : InstantaneousRecord {
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is HeartRateVariabilitySdnnRecord) return false
-
-        if (heartRateVariabilityMillis != other.heartRateVariabilityMillis) return false
-        if (time != other.time) return false
-        if (zoneOffset != other.zoneOffset) return false
-        if (metadata != other.metadata) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + heartRateVariabilityMillis.hashCode()
-        result = 31 * result + time.hashCode()
-        result = 31 * result + (zoneOffset?.hashCode() ?: 0)
-        result = 31 * result + metadata.hashCode()
-        return result
-    }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySdsdRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySdsdRecord.kt
deleted file mode 100644
index 76bf706..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilitySdsdRecord.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.records.metadata.Metadata
-import java.time.Instant
-import java.time.ZoneOffset
-
-/**
- * Captures user's heart rate variability (HRV) as measured by the standard deviation of differences
- * between adjacent NN intervals.
- *
- * @suppress
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class HeartRateVariabilitySdsdRecord(
-    override val time: Instant,
-    override val zoneOffset: ZoneOffset?,
-    /** Heart rate variability in milliseconds. Required field. */
-    public val heartRateVariabilityMillis: Double,
-    override val metadata: Metadata = Metadata.EMPTY,
-) : InstantaneousRecord {
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is HeartRateVariabilitySdsdRecord) return false
-
-        if (heartRateVariabilityMillis != other.heartRateVariabilityMillis) return false
-        if (time != other.time) return false
-        if (zoneOffset != other.zoneOffset) return false
-        if (metadata != other.metadata) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + heartRateVariabilityMillis.hashCode()
-        result = 31 * result + time.hashCode()
-        result = 31 * result + (zoneOffset?.hashCode() ?: 0)
-        result = 31 * result + metadata.hashCode()
-        return result
-    }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilityTinnRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilityTinnRecord.kt
deleted file mode 100644
index 6bf3bac..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilityTinnRecord.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.records.metadata.Metadata
-import java.time.Instant
-import java.time.ZoneOffset
-
-/**
- * Captures user's heart rate variability (HRV) as measured by the Triangular Interpolation
- * (baseline width) of a histogram displaying NN intervals.
- *
- * @suppress
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class HeartRateVariabilityTinnRecord(
-    override val time: Instant,
-    override val zoneOffset: ZoneOffset?,
-    /** Heart rate variability in milliseconds. Required field. */
-    public val heartRateVariabilityMillis: Double,
-    override val metadata: Metadata = Metadata.EMPTY,
-) : InstantaneousRecord {
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is HeartRateVariabilityTinnRecord) return false
-
-        if (heartRateVariabilityMillis != other.heartRateVariabilityMillis) return false
-        if (time != other.time) return false
-        if (zoneOffset != other.zoneOffset) return false
-        if (metadata != other.metadata) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + heartRateVariabilityMillis.hashCode()
-        result = 31 * result + time.hashCode()
-        result = 31 * result + (zoneOffset?.hashCode() ?: 0)
-        result = 31 * result + metadata.hashCode()
-        return result
-    }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HipCircumferenceRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HipCircumferenceRecord.kt
deleted file mode 100644
index bda2aaf..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HipCircumferenceRecord.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.records.metadata.Metadata
-import androidx.health.connect.client.units.Length
-import androidx.health.connect.client.units.meters
-import java.time.Instant
-import java.time.ZoneOffset
-
-/** Captures the user's hip circumference. */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class HipCircumferenceRecord(
-    override val time: Instant,
-    override val zoneOffset: ZoneOffset?,
-    /** Circumference in [Length] unit. Required field. Valid range: 0-10 meters. */
-    public val circumference: Length,
-    override val metadata: Metadata = Metadata.EMPTY,
-) : InstantaneousRecord {
-
-    init {
-        circumference.requireNotLess(other = circumference.zero(), name = "circumference")
-        circumference.requireNotMore(other = MAX_CIRCUMFERENCE, name = "circumference")
-    }
-
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
-     */
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is HipCircumferenceRecord) return false
-
-        if (circumference != other.circumference) return false
-        if (time != other.time) return false
-        if (zoneOffset != other.zoneOffset) return false
-        if (metadata != other.metadata) return false
-
-        return true
-    }
-
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
-     */
-    override fun hashCode(): Int {
-        var result = circumference.hashCode()
-        result = 31 * result + time.hashCode()
-        result = 31 * result + (zoneOffset?.hashCode() ?: 0)
-        result = 31 * result + metadata.hashCode()
-        return result
-    }
-
-    private companion object {
-        private val MAX_CIRCUMFERENCE = 10.meters
-    }
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SwimmingStrokesRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SwimmingStrokesRecord.kt
deleted file mode 100644
index 344eec9..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SwimmingStrokesRecord.kt
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.IntDef
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.records.metadata.Metadata
-import java.time.Instant
-import java.time.ZoneOffset
-
-/** Captures the number of swimming strokes. Type of swimming stroke must be provided. */
-public class SwimmingStrokesRecord(
-    override val startTime: Instant,
-    override val startZoneOffset: ZoneOffset?,
-    override val endTime: Instant,
-    override val endZoneOffset: ZoneOffset?,
-    /**
-     * Swimming style. Required field. Allowed values: [SwimmingType].
-     *
-     * @see SwimmingType
-     */
-    @property:SwimmingTypes public val type: Int,
-    /** Count of strokes. Optional field. Valid range: 1-1000000. */
-    public val count: Long = 0,
-    override val metadata: Metadata = Metadata.EMPTY,
-) : IntervalRecord {
-    init {
-        requireNonNegative(value = count, name = "count")
-        count.requireNotMore(other = 1000_000, name = "count")
-        require(startTime.isBefore(endTime)) { "startTime must be before endTime." }
-    }
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is SwimmingStrokesRecord) return false
-
-        if (count != other.count) return false
-        if (type != other.type) return false
-        if (startTime != other.startTime) return false
-        if (startZoneOffset != other.startZoneOffset) return false
-        if (endTime != other.endTime) return false
-        if (endZoneOffset != other.endZoneOffset) return false
-        if (metadata != other.metadata) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + count.hashCode()
-        result = 31 * result + type.hashCode()
-        result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
-        result = 31 * result + endTime.hashCode()
-        result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
-        result = 31 * result + metadata.hashCode()
-        return result
-    }
-
-    companion object {
-        const val SWIMMING_TYPE_OTHER = 0
-        const val SWIMMING_TYPE_FREESTYLE = 1
-        const val SWIMMING_TYPE_BACKSTROKE = 2
-        const val SWIMMING_TYPE_BREASTSTROKE = 3
-        const val SWIMMING_TYPE_BUTTERFLY = 4
-        const val SWIMMING_TYPE_MIXED = 5
-
-        /** Internal mappings useful for interoperability between integers and strings. */
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        @JvmField
-        val SWIMMING_TYPE_STRING_TO_INT_MAP: Map<String, Int> =
-            mapOf(
-                SwimmingType.FREESTYLE to SWIMMING_TYPE_FREESTYLE,
-                SwimmingType.BACKSTROKE to SWIMMING_TYPE_BACKSTROKE,
-                SwimmingType.BREASTSTROKE to SWIMMING_TYPE_BREASTSTROKE,
-                SwimmingType.BUTTERFLY to SWIMMING_TYPE_BUTTERFLY,
-                SwimmingType.MIXED to SWIMMING_TYPE_MIXED,
-            )
-
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
-        @JvmField
-        val SWIMMING_TYPE_INT_TO_STRING_MAP = SWIMMING_TYPE_STRING_TO_INT_MAP.reverse()
-    }
-
-    /** List of Swimming styles. */
-    internal object SwimmingType {
-        const val FREESTYLE = "freestyle"
-        const val BACKSTROKE = "backstroke"
-        const val BREASTSTROKE = "breaststroke"
-        const val BUTTERFLY = "butterfly"
-        const val MIXED = "mixed"
-        const val OTHER = "other"
-    }
-    /**
-     * Swimming styles.
-     * @suppress
-     */
-    @Retention(AnnotationRetention.SOURCE)
-    @IntDef(
-        value =
-            [
-                SWIMMING_TYPE_FREESTYLE,
-                SWIMMING_TYPE_BACKSTROKE,
-                SWIMMING_TYPE_BREASTSTROKE,
-                SWIMMING_TYPE_BUTTERFLY,
-                SWIMMING_TYPE_MIXED,
-                SWIMMING_TYPE_OTHER,
-            ]
-    )
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    annotation class SwimmingTypes
-}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/WaistCircumferenceRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/WaistCircumferenceRecord.kt
deleted file mode 100644
index c8e6244..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/WaistCircumferenceRecord.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.RestrictTo
-import androidx.health.connect.client.records.metadata.Metadata
-import androidx.health.connect.client.units.Length
-import androidx.health.connect.client.units.meters
-import java.time.Instant
-import java.time.ZoneOffset
-
-/**
- * Captures the user's waist circumference.
- *
- * See [Length] for supported units.
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class WaistCircumferenceRecord(
-    override val time: Instant,
-    override val zoneOffset: ZoneOffset?,
-    /** Circumference in [Length] unit. Required field. Valid range: 0-10 meters. */
-    public val circumference: Length,
-    override val metadata: Metadata = Metadata.EMPTY,
-) : InstantaneousRecord {
-
-    init {
-        circumference.requireNotLess(other = circumference.zero(), name = "circumference")
-        circumference.requireNotMore(other = MAX_CIRCUMFERENCE, name = "circumference")
-    }
-
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
-     */
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is WaistCircumferenceRecord) return false
-
-        if (circumference != other.circumference) return false
-        if (time != other.time) return false
-        if (zoneOffset != other.zoneOffset) return false
-        if (metadata != other.metadata) return false
-
-        return true
-    }
-
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
-     */
-    override fun hashCode(): Int {
-        var result = circumference.hashCode()
-        result = 31 * result + time.hashCode()
-        result = 31 * result + (zoneOffset?.hashCode() ?: 0)
-        result = 31 * result + metadata.hashCode()
-        return result
-    }
-
-    private companion object {
-        private val MAX_CIRCUMFERENCE = 10.meters
-    }
-}
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/PermissionControllerTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/PermissionControllerTest.kt
index fd39a8f..f73d84e 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/PermissionControllerTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/PermissionControllerTest.kt
@@ -41,11 +41,11 @@
     @Test
     fun createIntentTest() {
         val requestPermissionContract =
-            PermissionController.createRequestPermissionResultContract(PROVIDER_PACKAGE_NAME)
+            PermissionController.createRequestPermissionResultContractLegacy(PROVIDER_PACKAGE_NAME)
         val intent =
             requestPermissionContract.createIntent(
                 context,
-                setOf(HealthPermission.createReadPermission(StepsRecord::class))
+                setOf(HealthPermission.createReadPermissionLegacy(StepsRecord::class))
             )
 
         Truth.assertThat(intent.action).isEqualTo("androidx.health.ACTION_REQUEST_PERMISSIONS")
@@ -55,9 +55,7 @@
     @Test
     fun createIntentTest_permissionStrings() {
         val requestPermissionContract =
-            PermissionController.createRequestPermissionResultContractInternal(
-                PROVIDER_PACKAGE_NAME
-            )
+            PermissionController.createRequestPermissionResultContract(PROVIDER_PACKAGE_NAME)
         val intent =
             requestPermissionContract.createIntent(
                 context,
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 e09b709..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
@@ -25,7 +25,9 @@
 import androidx.health.connect.client.changes.DeletionChange
 import androidx.health.connect.client.changes.UpsertionChange
 import androidx.health.connect.client.impl.converters.datatype.toDataType
-import androidx.health.connect.client.permission.HealthPermission.Companion.createReadPermission
+import androidx.health.connect.client.permission.HealthPermission.Companion.createReadPermissionLegacy
+import androidx.health.connect.client.permission.HealthPermission.Companion.getReadPermission
+import androidx.health.connect.client.permission.HealthPermission.Companion.getWritePermission
 import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
 import androidx.health.connect.client.records.HeartRateRecord
 import androidx.health.connect.client.records.NutritionRecord
@@ -86,7 +88,7 @@
 
 private val API_METHOD_LIST =
     listOf<suspend HealthConnectClientImpl.() -> Unit>(
-        { getGrantedPermissions(setOf()) },
+        { getGrantedPermissionsLegacy(setOf()) },
         { revokeAllPermissions() },
         { insertRecords(listOf()) },
         { updateRecords(listOf()) },
@@ -141,7 +143,6 @@
 
         healthConnectClient =
             HealthConnectClientImpl(
-                PROVIDER_PACKAGE_NAME,
                 ServiceBackedHealthDataClient(
                     ApplicationProvider.getApplicationContext(),
                     clientConfig,
@@ -189,10 +190,10 @@
     }
 
     @Test
-    fun getGrantedPermissions_none() = runTest {
+    fun getGrantedPermissionsLegacy_none() = runTest {
         val response = testBlocking {
-            healthConnectClient.getGrantedPermissions(
-                setOf(createReadPermission(StepsRecord::class))
+            healthConnectClient.getGrantedPermissionsLegacy(
+                setOf(createReadPermissionLegacy(StepsRecord::class))
             )
         }
 
@@ -200,7 +201,7 @@
     }
 
     @Test
-    fun getGrantedPermissions_steps() = runTest {
+    fun getGrantedPermissionsLegacy_steps() = runTest {
         fakeAhpServiceStub.addGrantedPermission(
             androidx.health.platform.client.permission.Permission(
                 PermissionProto.Permission.newBuilder()
@@ -210,12 +211,44 @@
             )
         )
         val response = testBlocking {
-            healthConnectClient.getGrantedPermissions(
-                setOf(createReadPermission(StepsRecord::class))
+            healthConnectClient.getGrantedPermissionsLegacy(
+                setOf(createReadPermissionLegacy(StepsRecord::class))
             )
         }
 
-        assertThat(response).containsExactly(createReadPermission(StepsRecord::class))
+        assertThat(response).containsExactly(createReadPermissionLegacy(StepsRecord::class))
+    }
+
+    @Test
+    fun getGrantedPermissions_none() = runTest {
+        val response = testBlocking { healthConnectClient.getGrantedPermissions() }
+
+        assertThat(response).isEmpty()
+    }
+
+    @Test
+    fun getGrantedPermissions() = runTest {
+        fakeAhpServiceStub.addGrantedPermission(
+            androidx.health.platform.client.permission.Permission(
+                PermissionProto.Permission.newBuilder()
+                    .setPermission(getReadPermission(StepsRecord::class))
+                    .build()
+            )
+        )
+        fakeAhpServiceStub.addGrantedPermission(
+            androidx.health.platform.client.permission.Permission(
+                PermissionProto.Permission.newBuilder()
+                    .setPermission(getWritePermission(HeartRateRecord::class))
+                    .build()
+            )
+        )
+        val response = testBlocking { healthConnectClient.getGrantedPermissions() }
+
+        assertThat(response)
+            .containsExactly(
+                getReadPermission(StepsRecord::class),
+                getWritePermission(HeartRateRecord::class)
+            )
     }
 
     @Test
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
index 9246667..8730026 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
@@ -30,25 +30,12 @@
 import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
 import androidx.health.connect.client.records.DistanceRecord
 import androidx.health.connect.client.records.ElevationGainedRecord
-import androidx.health.connect.client.records.ExerciseEventRecord
-import androidx.health.connect.client.records.ExerciseLapRecord
-import androidx.health.connect.client.records.ExerciseRepetitionsRecord
-import androidx.health.connect.client.records.ExerciseRepetitionsRecord.Companion.REPETITION_TYPE_JUMPING_JACK
 import androidx.health.connect.client.records.ExerciseSessionRecord
 import androidx.health.connect.client.records.ExerciseSessionRecord.Companion.EXERCISE_TYPE_BACK_EXTENSION
 import androidx.health.connect.client.records.FloorsClimbedRecord
 import androidx.health.connect.client.records.HeartRateRecord
-import androidx.health.connect.client.records.HeartRateVariabilityDifferentialIndexRecord
 import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySd2Record
-import androidx.health.connect.client.records.HeartRateVariabilitySdannRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySdnnIndexRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySdnnRecord
-import androidx.health.connect.client.records.HeartRateVariabilitySdsdRecord
-import androidx.health.connect.client.records.HeartRateVariabilityTinnRecord
 import androidx.health.connect.client.records.HeightRecord
-import androidx.health.connect.client.records.HipCircumferenceRecord
 import androidx.health.connect.client.records.HydrationRecord
 import androidx.health.connect.client.records.IntermenstrualBleedingRecord
 import androidx.health.connect.client.records.LeanBodyMassRecord
@@ -69,11 +56,8 @@
 import androidx.health.connect.client.records.SpeedRecord
 import androidx.health.connect.client.records.StepsCadenceRecord
 import androidx.health.connect.client.records.StepsRecord
-import androidx.health.connect.client.records.SwimmingStrokesRecord
-import androidx.health.connect.client.records.SwimmingStrokesRecord.Companion.SWIMMING_TYPE_BACKSTROKE
 import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
 import androidx.health.connect.client.records.Vo2MaxRecord
-import androidx.health.connect.client.records.WaistCircumferenceRecord
 import androidx.health.connect.client.records.WeightRecord
 import androidx.health.connect.client.records.WheelchairPushesRecord
 import androidx.health.connect.client.records.metadata.DataOrigin
@@ -329,34 +313,6 @@
     }
 
     @Test
-    fun testHipCircumference() {
-        val data =
-            HipCircumferenceRecord(
-                circumference = 1.meters,
-                time = START_TIME,
-                zoneOffset = END_ZONE_OFFSET,
-                metadata = TEST_METADATA
-            )
-
-        checkProtoAndRecordTypeNameMatch(data)
-        assertThat(toRecord(data.toProto())).isEqualTo(data)
-    }
-
-    @Test
-    fun testHeartRateVariabilityDifferentialIndex() {
-        val data =
-            HeartRateVariabilityDifferentialIndexRecord(
-                heartRateVariabilityMillis = 1.0,
-                time = START_TIME,
-                zoneOffset = END_ZONE_OFFSET,
-                metadata = TEST_METADATA
-            )
-
-        checkProtoAndRecordTypeNameMatch(data)
-        assertThat(toRecord(data.toProto())).isEqualTo(data)
-    }
-
-    @Test
     fun testHeartRateVariabilityRmssd() {
         val data =
             HeartRateVariabilityRmssdRecord(
@@ -371,104 +327,6 @@
     }
 
     @Test
-    fun testHeartRateVariabilityS() {
-        val data =
-            HeartRateVariabilitySRecord(
-                heartRateVariabilityMillis = 1.0,
-                time = START_TIME,
-                zoneOffset = END_ZONE_OFFSET,
-                metadata = TEST_METADATA
-            )
-
-        checkProtoAndRecordTypeNameMatch(data)
-        assertThat(toRecord(data.toProto())).isEqualTo(data)
-    }
-
-    @Test
-    fun testHeartRateVariabilitySd2() {
-        val data =
-            HeartRateVariabilitySd2Record(
-                heartRateVariabilityMillis = 1.0,
-                time = START_TIME,
-                zoneOffset = END_ZONE_OFFSET,
-                metadata = TEST_METADATA
-            )
-
-        checkProtoAndRecordTypeNameMatch(data)
-        assertThat(toRecord(data.toProto())).isEqualTo(data)
-    }
-
-    @Test
-    fun testHeartRateVariabilitySdann() {
-        val data =
-            HeartRateVariabilitySdannRecord(
-                heartRateVariabilityMillis = 1.0,
-                time = START_TIME,
-                zoneOffset = END_ZONE_OFFSET,
-                metadata = TEST_METADATA
-            )
-
-        checkProtoAndRecordTypeNameMatch(data)
-        assertThat(toRecord(data.toProto())).isEqualTo(data)
-    }
-
-    @Test
-    fun testHeartRateVariabilitySdnnIndex() {
-        val data =
-            HeartRateVariabilitySdnnIndexRecord(
-                heartRateVariabilityMillis = 1.0,
-                time = START_TIME,
-                zoneOffset = END_ZONE_OFFSET,
-                metadata = TEST_METADATA
-            )
-
-        checkProtoAndRecordTypeNameMatch(data)
-        assertThat(toRecord(data.toProto())).isEqualTo(data)
-    }
-
-    @Test
-    fun testHeartRateVariabilitySdnn() {
-        val data =
-            HeartRateVariabilitySdnnRecord(
-                heartRateVariabilityMillis = 1.0,
-                time = START_TIME,
-                zoneOffset = END_ZONE_OFFSET,
-                metadata = TEST_METADATA
-            )
-
-        checkProtoAndRecordTypeNameMatch(data)
-        assertThat(toRecord(data.toProto())).isEqualTo(data)
-    }
-
-    @Test
-    fun testHeartRateVariabilitySdsd() {
-        val data =
-            HeartRateVariabilitySdsdRecord(
-                heartRateVariabilityMillis = 1.0,
-                time = START_TIME,
-                zoneOffset = END_ZONE_OFFSET,
-                metadata = TEST_METADATA
-            )
-
-        checkProtoAndRecordTypeNameMatch(data)
-        assertThat(toRecord(data.toProto())).isEqualTo(data)
-    }
-
-    @Test
-    fun testHeartRateVariabilityTinn() {
-        val data =
-            HeartRateVariabilityTinnRecord(
-                heartRateVariabilityMillis = 1.0,
-                time = START_TIME,
-                zoneOffset = END_ZONE_OFFSET,
-                metadata = TEST_METADATA
-            )
-
-        checkProtoAndRecordTypeNameMatch(data)
-        assertThat(toRecord(data.toProto())).isEqualTo(data)
-    }
-
-    @Test
     fun testIntermenstrualBleeding() {
         val data =
             IntermenstrualBleedingRecord(
@@ -688,20 +546,6 @@
     }
 
     @Test
-    fun testWaistCircumference() {
-        val data =
-            WaistCircumferenceRecord(
-                circumference = 1.meters,
-                time = START_TIME,
-                zoneOffset = END_ZONE_OFFSET,
-                metadata = TEST_METADATA
-            )
-
-        checkProtoAndRecordTypeNameMatch(data)
-        assertThat(toRecord(data.toProto())).isEqualTo(data)
-    }
-
-    @Test
     fun testWeight() {
         val data =
             WeightRecord(
@@ -732,38 +576,6 @@
     }
 
     @Test
-    fun testActivityEvent() {
-        val data =
-            ExerciseEventRecord(
-                eventType = ExerciseEventRecord.EVENT_TYPE_REST,
-                startTime = START_TIME,
-                startZoneOffset = START_ZONE_OFFSET,
-                endTime = END_TIME,
-                endZoneOffset = END_ZONE_OFFSET,
-                metadata = TEST_METADATA
-            )
-
-        checkProtoAndRecordTypeNameMatch(data)
-        assertThat(toRecord(data.toProto())).isEqualTo(data)
-    }
-
-    @Test
-    fun testActivityLap() {
-        val data =
-            ExerciseLapRecord(
-                length = 1.meters,
-                startTime = START_TIME,
-                startZoneOffset = START_ZONE_OFFSET,
-                endTime = END_TIME,
-                endZoneOffset = END_ZONE_OFFSET,
-                metadata = TEST_METADATA
-            )
-
-        checkProtoAndRecordTypeNameMatch(data)
-        assertThat(toRecord(data.toProto())).isEqualTo(data)
-    }
-
-    @Test
     fun testActivitySession() {
         val data =
             ExerciseSessionRecord(
@@ -905,23 +717,6 @@
     }
 
     @Test
-    fun testRepetitions() {
-        val data =
-            ExerciseRepetitionsRecord(
-                count = 1,
-                type = REPETITION_TYPE_JUMPING_JACK,
-                startTime = START_TIME,
-                startZoneOffset = START_ZONE_OFFSET,
-                endTime = END_TIME,
-                endZoneOffset = END_ZONE_OFFSET,
-                metadata = TEST_METADATA
-            )
-
-        checkProtoAndRecordTypeNameMatch(data)
-        assertThat(toRecord(data.toProto())).isEqualTo(data)
-    }
-
-    @Test
     fun testSleepSession() {
         val data =
             SleepSessionRecord(
@@ -971,23 +766,6 @@
     }
 
     @Test
-    fun testSwimmingStrokes() {
-        val data =
-            SwimmingStrokesRecord(
-                count = 1,
-                type = SWIMMING_TYPE_BACKSTROKE,
-                startTime = START_TIME,
-                startZoneOffset = START_ZONE_OFFSET,
-                endTime = END_TIME,
-                endZoneOffset = END_ZONE_OFFSET,
-                metadata = TEST_METADATA
-            )
-
-        checkProtoAndRecordTypeNameMatch(data)
-        assertThat(toRecord(data.toProto())).isEqualTo(data)
-    }
-
-    @Test
     fun testTotalCaloriesBurned() {
         val data =
             TotalCaloriesBurnedRecord(
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsTest.kt
index 796cbd4..7ef9575 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthDataRequestPermissionsTest.kt
@@ -47,7 +47,7 @@
         val intent =
             requestPermissionContract.createIntent(
                 context,
-                setOf(HealthPermission.createReadPermission(StepsRecord::class))
+                setOf(HealthPermission.createReadPermissionLegacy(StepsRecord::class))
             )
 
         assertThat(intent.action).isEqualTo("androidx.health.ACTION_REQUEST_PERMISSIONS")
@@ -60,7 +60,7 @@
         val intent =
             requestPermissionContract.createIntent(
                 context,
-                setOf(HealthPermission.createReadPermission(StepsRecord::class))
+                setOf(HealthPermission.createReadPermissionLegacy(StepsRecord::class))
             )
 
         assertThat(intent.action).isEqualTo("androidx.health.ACTION_REQUEST_PERMISSIONS")
@@ -101,7 +101,7 @@
         val result = requestPermissionContract.parseResult(0, intent)
 
         assertThat(result)
-            .containsExactly(HealthPermission.createReadPermission(StepsRecord::class))
+            .containsExactly(HealthPermission.createReadPermissionLegacy(StepsRecord::class))
     }
 
     @Test
@@ -110,7 +110,7 @@
         val result =
             requestPermissionContract.getSynchronousResult(
                 context,
-                setOf(HealthPermission.createReadPermission(StepsRecord::class))
+                setOf(HealthPermission.createReadPermissionLegacy(StepsRecord::class))
             )
 
         assertThat(result).isNull()
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthPermissionTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthPermissionTest.kt
index eb2649b..66d7068 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthPermissionTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthPermissionTest.kt
@@ -16,7 +16,6 @@
 package androidx.health.connect.client.permission
 
 import androidx.health.connect.client.RECORD_CLASSES
-import androidx.health.connect.client.records.ExerciseRouteRecord
 import androidx.health.connect.client.records.Record
 import androidx.health.connect.client.records.StepsRecord
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -29,60 +28,58 @@
 class HealthPermissionTest {
 
     @Test
-    fun createReadPermission() {
-        val permission = HealthPermission.createReadPermission(StepsRecord::class)
+    fun createReadPermissionLegacy() {
+        val permission = HealthPermission.createReadPermissionLegacy(StepsRecord::class)
         assertThat(permission.accessType).isEqualTo(AccessTypes.READ)
         assertThat(permission.recordType).isEqualTo(StepsRecord::class)
     }
 
     @Test
-    fun createWritePermission() {
-        val permission = HealthPermission.createWritePermission(StepsRecord::class)
+    fun createWritePermissionLegacy() {
+        val permission = HealthPermission.createWritePermissionLegacy(StepsRecord::class)
         assertThat(permission.accessType).isEqualTo(AccessTypes.WRITE)
         assertThat(permission.recordType).isEqualTo(StepsRecord::class)
     }
 
     @Test
-    fun createReadPermissionInternal() {
-        val permission = HealthPermission.createReadPermissionInternal(StepsRecord::class)
+    fun createReadPermission() {
+        val permission = HealthPermission.getReadPermission(StepsRecord::class)
         assertThat(permission).isEqualTo(HealthPermission.READ_STEPS)
     }
 
     @Test
-    fun createReadPermissionInternal_everyRecord() {
-        RECORD_CLASSES.filterNot { it == ExerciseRouteRecord::class }
-            .forEach {
-                val permission = HealthPermission.createReadPermissionInternal(it)
-                assertThat(permission).isNotNull()
-            }
-    }
-
-    @Test
-    fun createReadPermissionInternal_invalidRecord_isNull() {
-        assertThrows(IllegalArgumentException::class.java) {
-            HealthPermission.createReadPermissionInternal(Record::class)
+    fun createReadPermission_everyRecord() {
+        RECORD_CLASSES.forEach {
+            val permission = HealthPermission.getReadPermission(it)
+            assertThat(permission).isNotNull()
         }
     }
 
     @Test
-    fun createWritePermissionInternal() {
-        val permission = HealthPermission.createWritePermissionInternal(StepsRecord::class)
+    fun createReadPermission_invalidRecord_isNull() {
+        assertThrows(IllegalArgumentException::class.java) {
+            HealthPermission.getReadPermission(Record::class)
+        }
+    }
+
+    @Test
+    fun createWritePermission() {
+        val permission = HealthPermission.getWritePermission(StepsRecord::class)
         assertThat(permission).isEqualTo(HealthPermission.WRITE_STEPS)
     }
 
     @Test
-    fun createWritePermissionInternal_everyRecord() {
-        RECORD_CLASSES.filterNot { it == ExerciseRouteRecord::class }
-            .forEach {
-                val permission = HealthPermission.createWritePermissionInternal(it)
-                assertThat(permission).isNotNull()
-            }
+    fun createWritePermission_everyRecord() {
+        RECORD_CLASSES.forEach {
+            val permission = HealthPermission.getWritePermission(it)
+            assertThat(permission).isNotNull()
+        }
     }
 
     @Test
-    fun createWritePermissionInternal_invalidRecord_isNull() {
+    fun createWritePermission_invalidRecord_isNull() {
         assertThrows(IllegalArgumentException::class.java) {
-            HealthPermission.createWritePermissionInternal(Record::class)
+            HealthPermission.getWritePermission(Record::class)
         }
     }
 }
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseEventRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseEventRecordTest.kt
deleted file mode 100644
index 022994e..0000000
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseEventRecordTest.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.health.connect.client.records
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import java.time.Instant
-import kotlin.test.assertFailsWith
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class ExerciseEventRecordTest {
-
-    @Test
-    fun validRecord_equals() {
-        assertThat(
-                ExerciseEventRecord(
-                    startTime = Instant.ofEpochMilli(1234L),
-                    startZoneOffset = null,
-                    endTime = Instant.ofEpochMilli(1236L),
-                    endZoneOffset = null,
-                    eventType = ExerciseEventRecord.EVENT_TYPE_REST,
-                )
-            )
-            .isEqualTo(
-                ExerciseEventRecord(
-                    startTime = Instant.ofEpochMilli(1234L),
-                    startZoneOffset = null,
-                    endTime = Instant.ofEpochMilli(1236L),
-                    endZoneOffset = null,
-                    eventType = ExerciseEventRecord.EVENT_TYPE_REST,
-                )
-            )
-    }
-
-    @Test
-    fun invalidTimes_throws() {
-        assertFailsWith<IllegalArgumentException> {
-            ExerciseEventRecord(
-                startTime = Instant.ofEpochMilli(1234L),
-                startZoneOffset = null,
-                endTime = Instant.ofEpochMilli(1234L),
-                endZoneOffset = null,
-                eventType = ExerciseEventRecord.EVENT_TYPE_REST,
-            )
-        }
-    }
-
-    @Test
-    fun eventEnums_existInMapping() {
-        val allEnums = ExerciseEventRecord.Companion::class.allIntDefEnumsWithPrefix("EVENT_TYPE")
-
-        assertThat(ExerciseEventRecord.EVENT_TYPE_STRING_TO_INT_MAP.values)
-            .containsExactlyElementsIn(allEnums)
-        assertThat(ExerciseEventRecord.EVENT_TYPE_INT_TO_STRING_MAP.keys)
-            .containsExactlyElementsIn(allEnums)
-    }
-}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseEventRepetitionsTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseEventRepetitionsTest.kt
deleted file mode 100644
index 517e7b4..0000000
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseEventRepetitionsTest.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.health.connect.client.records
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import java.time.Instant
-import kotlin.reflect.typeOf
-import kotlin.test.assertFailsWith
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class ExerciseEventRepetitionsTest {
-
-    @Test
-    fun validRecord_equals() {
-        assertThat(
-                ExerciseRepetitionsRecord(
-                    startTime = Instant.ofEpochMilli(1234L),
-                    startZoneOffset = null,
-                    endTime = Instant.ofEpochMilli(1236L),
-                    endZoneOffset = null,
-                    count = 10,
-                    type = ExerciseRepetitionsRecord.REPETITION_TYPE_ARM_CURL,
-                )
-            )
-            .isEqualTo(
-                ExerciseRepetitionsRecord(
-                    startTime = Instant.ofEpochMilli(1234L),
-                    startZoneOffset = null,
-                    endTime = Instant.ofEpochMilli(1236L),
-                    endZoneOffset = null,
-                    count = 10,
-                    type = ExerciseRepetitionsRecord.REPETITION_TYPE_ARM_CURL,
-                )
-            )
-    }
-
-    @Test
-    fun invalidTimes_throws() {
-        assertFailsWith<IllegalArgumentException> {
-            ExerciseRepetitionsRecord(
-                startTime = Instant.ofEpochMilli(1234L),
-                startZoneOffset = null,
-                endTime = Instant.ofEpochMilli(1234L),
-                endZoneOffset = null,
-                count = 10,
-                type = ExerciseRepetitionsRecord.REPETITION_TYPE_ARM_CURL,
-            )
-        }
-    }
-
-    @Test
-    fun allRepetitions_hasMapping() {
-        val allEnums =
-            ExerciseRepetitionsRecord.Companion::class
-                .members
-                .asSequence()
-                .filter { it ->
-                    it.name.startsWith("REPETITION_TYPE") && !it.name.endsWith("UNKNOWN")
-                }
-                .filter { it -> it.returnType == typeOf<Int>() }
-                .map { it -> it.call(ExerciseSessionRecord.Companion) }
-                .toHashSet()
-
-        assertThat(ExerciseRepetitionsRecord.REPETITION_TYPE_STRING_TO_INT_MAP.values)
-            .containsExactlyElementsIn(allEnums)
-        assertThat(ExerciseRepetitionsRecord.REPETITION_TYPE_INT_TO_STRING_MAP.keys)
-            .containsExactlyElementsIn(allEnums)
-    }
-}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseLapRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseLapRecordTest.kt
deleted file mode 100644
index 68c96e6..0000000
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseLapRecordTest.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.health.connect.client.records
-
-import androidx.health.connect.client.units.meters
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import java.time.Instant
-import kotlin.test.assertFailsWith
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class ExerciseLapRecordTest {
-
-    @Test
-    fun validRecord_equals() {
-        assertThat(
-                ExerciseLapRecord(
-                    startTime = Instant.ofEpochMilli(1234L),
-                    startZoneOffset = null,
-                    endTime = Instant.ofEpochMilli(1236L),
-                    endZoneOffset = null,
-                    length = 10.0.meters,
-                )
-            )
-            .isEqualTo(
-                ExerciseLapRecord(
-                    startTime = Instant.ofEpochMilli(1234L),
-                    startZoneOffset = null,
-                    endTime = Instant.ofEpochMilli(1236L),
-                    endZoneOffset = null,
-                    length = 10.0.meters,
-                )
-            )
-    }
-
-    @Test
-    fun invalidTimes_throws() {
-        assertFailsWith<IllegalArgumentException> {
-            ExerciseLapRecord(
-                startTime = Instant.ofEpochMilli(1234L),
-                startZoneOffset = null,
-                endTime = Instant.ofEpochMilli(1234L),
-                endZoneOffset = null,
-                length = 10.0.meters,
-            )
-        }
-    }
-}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SwimmingStrokesRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SwimmingStrokesRecordTest.kt
deleted file mode 100644
index 8428f38..0000000
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SwimmingStrokesRecordTest.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.health.connect.client.records
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import java.time.Instant
-import kotlin.test.assertFailsWith
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class SwimmingStrokesRecordTest {
-
-    @Test
-    fun validRecord_equals() {
-        assertThat(
-                SwimmingStrokesRecord(
-                    startTime = Instant.ofEpochMilli(1234L),
-                    startZoneOffset = null,
-                    endTime = Instant.ofEpochMilli(1236L),
-                    endZoneOffset = null,
-                    count = 10,
-                    type = SwimmingStrokesRecord.SWIMMING_TYPE_BACKSTROKE,
-                )
-            )
-            .isEqualTo(
-                SwimmingStrokesRecord(
-                    startTime = Instant.ofEpochMilli(1234L),
-                    startZoneOffset = null,
-                    endTime = Instant.ofEpochMilli(1236L),
-                    endZoneOffset = null,
-                    count = 10,
-                    type = SwimmingStrokesRecord.SWIMMING_TYPE_BACKSTROKE,
-                )
-            )
-    }
-
-    @Test
-    fun invalidTimes_throws() {
-        assertFailsWith<IllegalArgumentException> {
-            SwimmingStrokesRecord(
-                startTime = Instant.ofEpochMilli(1234L),
-                startZoneOffset = null,
-                endTime = Instant.ofEpochMilli(1234L),
-                endZoneOffset = null,
-                count = 10,
-                type = SwimmingStrokesRecord.SWIMMING_TYPE_BACKSTROKE,
-            )
-        }
-    }
-
-    @Test
-    fun eventEnums_existInMapping() {
-        val allEnums =
-            SwimmingStrokesRecord.Companion::class.allIntDefEnumsWithPrefix("SWIMMING_TYPE")
-                .filter { it != SwimmingStrokesRecord.SWIMMING_TYPE_OTHER }
-
-        assertThat(SwimmingStrokesRecord.SWIMMING_TYPE_STRING_TO_INT_MAP.values)
-            .containsExactlyElementsIn(allEnums)
-        assertThat(SwimmingStrokesRecord.SWIMMING_TYPE_INT_TO_STRING_MAP.keys)
-            .containsExactlyElementsIn(allEnums)
-    }
-}
diff --git a/health/health-services-client/api/1.0.0-beta03.txt b/health/health-services-client/api/1.0.0-beta03.txt
new file mode 100644
index 0000000..9d0902d
--- /dev/null
+++ b/health/health-services-client/api/1.0.0-beta03.txt
@@ -0,0 +1,909 @@
+// Signature format: 4.0
+package androidx.health.services.client {
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface ExerciseClient {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> addGoalToActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback callback);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> endExerciseAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> flushAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.health.services.client.data.ExerciseCapabilities> getCapabilitiesAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.health.services.client.data.ExerciseInfo> getCurrentExerciseInfoAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> markLapAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> overrideAutoPauseAndResumeForActiveExerciseAsync(boolean enabled);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> overrideBatchingModesForActiveExerciseAsync(java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModes);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> pauseExerciseAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> prepareExerciseAsync(androidx.health.services.client.data.WarmUpConfig configuration);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> removeGoalFromActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> resumeExerciseAsync();
+    method public void setUpdateCallback(androidx.health.services.client.ExerciseUpdateCallback callback);
+    method public void setUpdateCallback(java.util.concurrent.Executor executor, androidx.health.services.client.ExerciseUpdateCallback callback);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig);
+  }
+
+  public final class ExerciseClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? addGoalToActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? clearUpdateCallback(androidx.health.services.client.ExerciseClient, androidx.health.services.client.ExerciseUpdateCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? endExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? flush(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? getCapabilities(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseCapabilities>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? getCurrentExerciseInfo(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseInfo>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? markLap(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? overrideAutoPauseAndResumeForActiveExercise(androidx.health.services.client.ExerciseClient, boolean enabled, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? pauseExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? prepareExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.WarmUpConfig configuration, kotlin.coroutines.Continuation<? super kotlin.Unit>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super kotlin.Unit>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? updateExerciseTypeConfig(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+  }
+
+  public interface ExerciseUpdateCallback {
+    method public void onAvailabilityChanged(androidx.health.services.client.data.DataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
+    method public void onExerciseUpdateReceived(androidx.health.services.client.data.ExerciseUpdate update);
+    method public void onLapSummaryReceived(androidx.health.services.client.data.ExerciseLapSummary lapSummary);
+    method public void onRegistered();
+    method public void onRegistrationFailed(Throwable throwable);
+  }
+
+  public final class HealthServices {
+    method public static androidx.health.services.client.HealthServicesClient getClient(android.content.Context context);
+    field public static final androidx.health.services.client.HealthServices INSTANCE;
+  }
+
+  public interface HealthServicesClient {
+    method public androidx.health.services.client.ExerciseClient getExerciseClient();
+    method public androidx.health.services.client.MeasureClient getMeasureClient();
+    method public androidx.health.services.client.PassiveMonitoringClient getPassiveMonitoringClient();
+    property public abstract androidx.health.services.client.ExerciseClient exerciseClient;
+    property public abstract androidx.health.services.client.MeasureClient measureClient;
+    property public abstract androidx.health.services.client.PassiveMonitoringClient passiveMonitoringClient;
+  }
+
+  public final class HealthServicesException extends java.lang.Exception {
+    ctor public HealthServicesException(String message);
+  }
+
+  public final class ListenableFutureExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend <T> Object? awaitWithException(com.google.common.util.concurrent.ListenableFuture<T>, kotlin.coroutines.Continuation<? super T>) throws androidx.health.services.client.HealthServicesException;
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureCallback {
+    method public void onAvailabilityChanged(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
+    method public void onDataReceived(androidx.health.services.client.data.DataPointContainer data);
+    method public default void onRegistered();
+    method public default void onRegistrationFailed(Throwable throwable);
+  }
+
+  public interface MeasureClient {
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.health.services.client.data.MeasureCapabilities> getCapabilitiesAsync();
+    method public void registerMeasureCallback(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
+    method public void registerMeasureCallback(androidx.health.services.client.data.DeltaDataType<?,?> dataType, java.util.concurrent.Executor executor, androidx.health.services.client.MeasureCallback callback);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> unregisterMeasureCallbackAsync(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
+  }
+
+  public final class MeasureClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? getCapabilities(androidx.health.services.client.MeasureClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.MeasureCapabilities>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface PassiveListenerCallback {
+    method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
+    method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
+    method public default void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
+    method public default void onPermissionLost();
+    method public default void onRegistered();
+    method public default void onRegistrationFailed(Throwable throwable);
+    method public default void onUserActivityInfoReceived(androidx.health.services.client.data.UserActivityInfo info);
+  }
+
+  public abstract class PassiveListenerService extends android.app.Service {
+    ctor public PassiveListenerService();
+    method public final android.os.IBinder? onBind(android.content.Intent intent);
+    method public void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
+    method public void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
+    method public void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
+    method public void onPermissionLost();
+    method public void onUserActivityInfoReceived(androidx.health.services.client.data.UserActivityInfo info);
+  }
+
+  public interface PassiveMonitoringClient {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearPassiveListenerCallbackAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearPassiveListenerServiceAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> flushAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.health.services.client.data.PassiveMonitoringCapabilities> getCapabilitiesAsync();
+    method public void setPassiveListenerCallback(androidx.health.services.client.data.PassiveListenerConfig config, androidx.health.services.client.PassiveListenerCallback callback);
+    method public void setPassiveListenerCallback(androidx.health.services.client.data.PassiveListenerConfig config, java.util.concurrent.Executor executor, androidx.health.services.client.PassiveListenerCallback callback);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
+  }
+
+  public final class PassiveMonitoringClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? clearPassiveListenerCallback(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? clearPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? flush(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? getCapabilities(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.PassiveMonitoringCapabilities>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? setPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+  }
+
+}
+
+package androidx.health.services.client.data {
+
+  public final class AggregateDataType<T extends java.lang.Number, D extends androidx.health.services.client.data.DataPoint<T>> extends androidx.health.services.client.data.DataType<T,D> {
+    ctor public AggregateDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface Availability {
+    method public int getId();
+    property public abstract int id;
+    field public static final androidx.health.services.client.data.Availability.Companion Companion;
+  }
+
+  public static final class Availability.Companion {
+  }
+
+  public final class BatchingMode {
+    field public static final androidx.health.services.client.data.BatchingMode.Companion Companion;
+    field public static final androidx.health.services.client.data.BatchingMode HEART_RATE_5_SECONDS;
+  }
+
+  public static final class BatchingMode.Companion {
+  }
+
+  public final class ComparisonType {
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.ComparisonType.Companion Companion;
+    field public static final androidx.health.services.client.data.ComparisonType GREATER_THAN;
+    field public static final androidx.health.services.client.data.ComparisonType GREATER_THAN_OR_EQUAL;
+    field public static final androidx.health.services.client.data.ComparisonType LESS_THAN;
+    field public static final androidx.health.services.client.data.ComparisonType LESS_THAN_OR_EQUAL;
+    field public static final androidx.health.services.client.data.ComparisonType UNKNOWN;
+  }
+
+  public static final class ComparisonType.Companion {
+  }
+
+  public final class CumulativeDataPoint<T extends java.lang.Number> extends androidx.health.services.client.data.DataPoint<T> {
+    ctor public CumulativeDataPoint(androidx.health.services.client.data.AggregateDataType<T,androidx.health.services.client.data.CumulativeDataPoint<T>> dataType, T total, java.time.Instant start, java.time.Instant end);
+    method public java.time.Instant getEnd();
+    method public java.time.Instant getStart();
+    method public T getTotal();
+    property public final java.time.Instant end;
+    property public final java.time.Instant start;
+    property public final T total;
+  }
+
+  public abstract class DataPoint<T> {
+    method public androidx.health.services.client.data.DataType<T,? extends androidx.health.services.client.data.DataPoint<T>> getDataType();
+    property public androidx.health.services.client.data.DataType<T,? extends androidx.health.services.client.data.DataPoint<T>> dataType;
+  }
+
+  public abstract class DataPointAccuracy {
+    ctor public DataPointAccuracy();
+  }
+
+  public final class DataPointContainer {
+    ctor public DataPointContainer(java.util.Map<androidx.health.services.client.data.DataType<?,?>,? extends java.util.List<? extends androidx.health.services.client.data.DataPoint<?>>> dataPoints);
+    ctor public DataPointContainer(java.util.List<? extends androidx.health.services.client.data.DataPoint<?>> dataPointList);
+    method public java.util.List<androidx.health.services.client.data.CumulativeDataPoint<?>> getCumulativeDataPoints();
+    method public <T, D extends androidx.health.services.client.data.DataPoint<T>> java.util.List<D> getData(androidx.health.services.client.data.DeltaDataType<T,D> type);
+    method public <T extends java.lang.Number, D extends androidx.health.services.client.data.DataPoint<T>> D? getData(androidx.health.services.client.data.AggregateDataType<T,D> type);
+    method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
+    method public java.util.List<androidx.health.services.client.data.IntervalDataPoint<?>> getIntervalDataPoints();
+    method public java.util.List<androidx.health.services.client.data.SampleDataPoint<?>> getSampleDataPoints();
+    method public java.util.List<androidx.health.services.client.data.StatisticalDataPoint<?>> getStatisticalDataPoints();
+    property public final java.util.List<androidx.health.services.client.data.CumulativeDataPoint<?>> cumulativeDataPoints;
+    property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> dataTypes;
+    property public final java.util.List<androidx.health.services.client.data.IntervalDataPoint<?>> intervalDataPoints;
+    property public final java.util.List<androidx.health.services.client.data.SampleDataPoint<?>> sampleDataPoints;
+    property public final java.util.List<androidx.health.services.client.data.StatisticalDataPoint<?>> statisticalDataPoints;
+  }
+
+  public abstract class DataType<T, D extends androidx.health.services.client.data.DataPoint<T>> {
+    ctor public DataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass, boolean isAggregate);
+    method public final String getName();
+    method public final Class<T> getValueClass();
+    property public final String name;
+    property public final Class<T> valueClass;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> ABSOLUTE_ELEVATION;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> ABSOLUTE_ELEVATION_STATS;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> ACTIVE_EXERCISE_DURATION_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> CALORIES;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> CALORIES_DAILY;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> CALORIES_TOTAL;
+    field public static final androidx.health.services.client.data.DataType.Companion Companion;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> DECLINE_DISTANCE;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> DECLINE_DISTANCE_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> DECLINE_DURATION;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> DECLINE_DURATION_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> DISTANCE;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> DISTANCE_DAILY;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> DISTANCE_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> ELEVATION_GAIN;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> ELEVATION_GAIN_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> ELEVATION_LOSS;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> ELEVATION_LOSS_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> FLAT_GROUND_DISTANCE;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> FLAT_GROUND_DISTANCE_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> FLAT_GROUND_DURATION;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> FLAT_GROUND_DURATION_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> FLOORS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> FLOORS_DAILY;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> FLOORS_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> GOLF_SHOT_COUNT;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> GOLF_SHOT_COUNT_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> HEART_RATE_BPM;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> HEART_RATE_BPM_STATS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> INCLINE_DISTANCE;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> INCLINE_DISTANCE_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> INCLINE_DURATION;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> INCLINE_DURATION_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<androidx.health.services.client.data.LocationData,androidx.health.services.client.data.SampleDataPoint<androidx.health.services.client.data.LocationData>> LOCATION;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> PACE;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> PACE_STATS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> REP_COUNT;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> REP_COUNT_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> RESTING_EXERCISE_DURATION;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> RESTING_EXERCISE_DURATION_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> RUNNING_STEPS;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> RUNNING_STEPS_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> SPEED;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> SPEED_STATS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> STEPS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> STEPS_DAILY;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.SampleDataPoint<java.lang.Long>> STEPS_PER_MINUTE;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Long>> STEPS_PER_MINUTE_STATS;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> STEPS_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> SWIMMING_LAP_COUNT;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> SWIMMING_STROKES;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> SWIMMING_STROKES_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> VO2_MAX;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> VO2_MAX_STATS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> WALKING_STEPS;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> WALKING_STEPS_TOTAL;
+  }
+
+  public static final class DataType.Companion {
+  }
+
+  public static final class DataType.TimeType {
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.DataType.TimeType.Companion Companion;
+    field public static final androidx.health.services.client.data.DataType.TimeType INTERVAL;
+    field public static final androidx.health.services.client.data.DataType.TimeType SAMPLE;
+    field public static final androidx.health.services.client.data.DataType.TimeType UNKNOWN;
+  }
+
+  public static final class DataType.TimeType.Companion {
+  }
+
+  public final class DataTypeAvailability implements androidx.health.services.client.data.Availability {
+    method public static androidx.health.services.client.data.DataTypeAvailability? fromId(int id);
+    method public int getId();
+    method public String getName();
+    property public int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.DataTypeAvailability ACQUIRING;
+    field public static final androidx.health.services.client.data.DataTypeAvailability AVAILABLE;
+    field public static final androidx.health.services.client.data.DataTypeAvailability.Companion Companion;
+    field public static final androidx.health.services.client.data.DataTypeAvailability UNAVAILABLE;
+    field public static final androidx.health.services.client.data.DataTypeAvailability UNAVAILABLE_DEVICE_OFF_BODY;
+    field public static final androidx.health.services.client.data.DataTypeAvailability UNKNOWN;
+  }
+
+  public static final class DataTypeAvailability.Companion {
+    method public androidx.health.services.client.data.DataTypeAvailability? fromId(int id);
+  }
+
+  public final class DataTypeCondition<T extends java.lang.Number, D extends androidx.health.services.client.data.DataType<T, ? extends androidx.health.services.client.data.DataPoint<T>>> {
+    ctor public DataTypeCondition(D dataType, T threshold, androidx.health.services.client.data.ComparisonType comparisonType);
+    method public androidx.health.services.client.data.ComparisonType getComparisonType();
+    method public D getDataType();
+    method public T getThreshold();
+    property public final androidx.health.services.client.data.ComparisonType comparisonType;
+    property public final D dataType;
+    property public final T threshold;
+  }
+
+  public final class DeltaDataType<T, D extends androidx.health.services.client.data.DataPoint<T>> extends androidx.health.services.client.data.DataType<T,D> {
+    ctor public DeltaDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
+  }
+
+  public final class ExerciseCapabilities {
+    ctor public ExerciseCapabilities(java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities, optional java.util.Set<androidx.health.services.client.data.BatchingMode> supportedBatchingModeOverrides);
+    ctor public ExerciseCapabilities(java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities);
+    method public java.util.Set<androidx.health.services.client.data.ExerciseType> getAutoPauseAndResumeEnabledExercises();
+    method public androidx.health.services.client.data.ExerciseTypeCapabilities getExerciseTypeCapabilities(androidx.health.services.client.data.ExerciseType exercise);
+    method public java.util.Set<androidx.health.services.client.data.BatchingMode> getSupportedBatchingModeOverrides();
+    method public java.util.Set<androidx.health.services.client.data.ExerciseType> getSupportedExerciseTypes();
+    method public java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> getTypeToCapabilities();
+    property public final java.util.Set<androidx.health.services.client.data.ExerciseType> autoPauseAndResumeEnabledExercises;
+    property public final java.util.Set<androidx.health.services.client.data.BatchingMode> supportedBatchingModeOverrides;
+    property public final java.util.Set<androidx.health.services.client.data.ExerciseType> supportedExerciseTypes;
+    property public final java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities;
+  }
+
+  public final class ExerciseConfig {
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig, optional java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
+    method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
+    method public java.util.Set<androidx.health.services.client.data.BatchingMode> getBatchingModeOverrides();
+    method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
+    method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
+    method public android.os.Bundle getExerciseParams();
+    method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    method public androidx.health.services.client.data.ExerciseTypeConfig? getExerciseTypeConfig();
+    method public float getSwimmingPoolLengthMeters();
+    method public boolean isAutoPauseAndResumeEnabled();
+    method public boolean isGpsEnabled();
+    property public final java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides;
+    property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> dataTypes;
+    property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
+    property public final android.os.Bundle exerciseParams;
+    property public final androidx.health.services.client.data.ExerciseType exerciseType;
+    property public final androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig;
+    property public final boolean isAutoPauseAndResumeEnabled;
+    property public final boolean isGpsEnabled;
+    property public final float swimmingPoolLengthMeters;
+    field public static final androidx.health.services.client.data.ExerciseConfig.Companion Companion;
+    field public static final float SWIMMING_POOL_LENGTH_UNSPECIFIED = 0.0f;
+  }
+
+  public static final class ExerciseConfig.Builder {
+    ctor public ExerciseConfig.Builder(androidx.health.services.client.data.ExerciseType exerciseType);
+    method public androidx.health.services.client.data.ExerciseConfig build();
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setBatchingModeOverrides(java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseTypeConfig(androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLengthMeters(float swimmingPoolLength);
+  }
+
+  public static final class ExerciseConfig.Companion {
+    method public androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
+  }
+
+  public final class ExerciseGoal<T extends java.lang.Number> implements android.os.Parcelable {
+    method public static <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createMilestone(androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> condition, T period);
+    method public static <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createMilestoneGoalWithUpdatedThreshold(androidx.health.services.client.data.ExerciseGoal<T> goal, T newThreshold);
+    method public static <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createOneTimeGoal(androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> condition);
+    method public int describeContents();
+    method public androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> getDataTypeCondition();
+    method public androidx.health.services.client.data.ExerciseGoalType getExerciseGoalType();
+    method public T? getPeriod();
+    method public void writeToParcel(android.os.Parcel dest, int flags);
+    property public final androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> dataTypeCondition;
+    property public final androidx.health.services.client.data.ExerciseGoalType exerciseGoalType;
+    property public final T? period;
+    field public static final android.os.Parcelable.Creator<androidx.health.services.client.data.ExerciseGoal<?>> CREATOR;
+    field public static final androidx.health.services.client.data.ExerciseGoal.Companion Companion;
+  }
+
+  public static final class ExerciseGoal.Companion {
+    method public <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createMilestone(androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> condition, T period);
+    method public <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createMilestoneGoalWithUpdatedThreshold(androidx.health.services.client.data.ExerciseGoal<T> goal, T newThreshold);
+    method public <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createOneTimeGoal(androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> condition);
+  }
+
+  public final class ExerciseGoalType {
+    method public static androidx.health.services.client.data.ExerciseGoalType? fromId(int id);
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.ExerciseGoalType.Companion Companion;
+    field public static final androidx.health.services.client.data.ExerciseGoalType MILESTONE;
+    field public static final androidx.health.services.client.data.ExerciseGoalType ONE_TIME_GOAL;
+  }
+
+  public static final class ExerciseGoalType.Companion {
+    method public androidx.health.services.client.data.ExerciseGoalType? fromId(int id);
+  }
+
+  public final class ExerciseInfo {
+    ctor public ExerciseInfo(int exerciseTrackedStatus, androidx.health.services.client.data.ExerciseType exerciseType);
+    method public int getExerciseTrackedStatus();
+    method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    property public final int exerciseTrackedStatus;
+    property public final androidx.health.services.client.data.ExerciseType exerciseType;
+  }
+
+  public final class ExerciseLapSummary {
+    ctor public ExerciseLapSummary(int lapCount, java.time.Instant startTime, java.time.Instant endTime, java.time.Duration activeDuration, androidx.health.services.client.data.DataPointContainer lapMetrics);
+    method public java.time.Duration getActiveDuration();
+    method public java.time.Instant getEndTime();
+    method public int getLapCount();
+    method public androidx.health.services.client.data.DataPointContainer getLapMetrics();
+    method public java.time.Instant getStartTime();
+    property public final java.time.Duration activeDuration;
+    property public final java.time.Instant endTime;
+    property public final int lapCount;
+    property public final androidx.health.services.client.data.DataPointContainer lapMetrics;
+    property public final java.time.Instant startTime;
+  }
+
+  public final class ExerciseState {
+    method public static androidx.health.services.client.data.ExerciseState? fromId(int id);
+    method public int getId();
+    method public String getName();
+    method public boolean isEnded();
+    method public boolean isEnding();
+    method public boolean isPaused();
+    method public boolean isResuming();
+    property public final int id;
+    property public final boolean isEnded;
+    property public final boolean isEnding;
+    property public final boolean isPaused;
+    property public final boolean isResuming;
+    property public final String name;
+    field public static final androidx.health.services.client.data.ExerciseState ACTIVE;
+    field public static final androidx.health.services.client.data.ExerciseState AUTO_PAUSED;
+    field public static final androidx.health.services.client.data.ExerciseState AUTO_PAUSING;
+    field public static final androidx.health.services.client.data.ExerciseState AUTO_RESUMING;
+    field public static final androidx.health.services.client.data.ExerciseState.Companion Companion;
+    field public static final androidx.health.services.client.data.ExerciseState ENDED;
+    field public static final androidx.health.services.client.data.ExerciseState ENDING;
+    field public static final androidx.health.services.client.data.ExerciseState PREPARING;
+    field public static final androidx.health.services.client.data.ExerciseState USER_PAUSED;
+    field public static final androidx.health.services.client.data.ExerciseState USER_PAUSING;
+    field public static final androidx.health.services.client.data.ExerciseState USER_RESUMING;
+    field public static final androidx.health.services.client.data.ExerciseState USER_STARTING;
+  }
+
+  public static final class ExerciseState.Companion {
+    method public androidx.health.services.client.data.ExerciseState? fromId(int id);
+  }
+
+  public final class ExerciseStateInfo {
+    ctor public ExerciseStateInfo(androidx.health.services.client.data.ExerciseState exerciseState, int exerciseEndReason);
+    method public int getEndReason();
+    method public androidx.health.services.client.data.ExerciseState getState();
+    property public final int endReason;
+    property public final androidx.health.services.client.data.ExerciseState state;
+    field public static final androidx.health.services.client.data.ExerciseStateInfo.Companion Companion;
+  }
+
+  public static final class ExerciseStateInfo.Companion {
+  }
+
+  public final class ExerciseType {
+    method public static androidx.health.services.client.data.ExerciseType fromId(int id);
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.ExerciseType ALPINE_SKIING;
+    field public static final androidx.health.services.client.data.ExerciseType BACKPACKING;
+    field public static final androidx.health.services.client.data.ExerciseType BACK_EXTENSION;
+    field public static final androidx.health.services.client.data.ExerciseType BADMINTON;
+    field public static final androidx.health.services.client.data.ExerciseType BARBELL_SHOULDER_PRESS;
+    field public static final androidx.health.services.client.data.ExerciseType BASEBALL;
+    field public static final androidx.health.services.client.data.ExerciseType BASKETBALL;
+    field public static final androidx.health.services.client.data.ExerciseType BENCH_PRESS;
+    field public static final androidx.health.services.client.data.ExerciseType BIKING;
+    field public static final androidx.health.services.client.data.ExerciseType BIKING_STATIONARY;
+    field public static final androidx.health.services.client.data.ExerciseType BOOT_CAMP;
+    field public static final androidx.health.services.client.data.ExerciseType BOXING;
+    field public static final androidx.health.services.client.data.ExerciseType BURPEE;
+    field public static final androidx.health.services.client.data.ExerciseType CALISTHENICS;
+    field public static final androidx.health.services.client.data.ExerciseType CRICKET;
+    field public static final androidx.health.services.client.data.ExerciseType CROSS_COUNTRY_SKIING;
+    field public static final androidx.health.services.client.data.ExerciseType CRUNCH;
+    field public static final androidx.health.services.client.data.ExerciseType.Companion Companion;
+    field public static final androidx.health.services.client.data.ExerciseType DANCING;
+    field public static final androidx.health.services.client.data.ExerciseType DEADLIFT;
+    field public static final androidx.health.services.client.data.ExerciseType ELLIPTICAL;
+    field public static final androidx.health.services.client.data.ExerciseType EXERCISE_CLASS;
+    field public static final androidx.health.services.client.data.ExerciseType FENCING;
+    field public static final androidx.health.services.client.data.ExerciseType FOOTBALL_AMERICAN;
+    field public static final androidx.health.services.client.data.ExerciseType FOOTBALL_AUSTRALIAN;
+    field public static final androidx.health.services.client.data.ExerciseType FORWARD_TWIST;
+    field public static final androidx.health.services.client.data.ExerciseType FRISBEE_DISC;
+    field public static final androidx.health.services.client.data.ExerciseType GOLF;
+    field public static final androidx.health.services.client.data.ExerciseType GUIDED_BREATHING;
+    field public static final androidx.health.services.client.data.ExerciseType GYMNASTICS;
+    field public static final androidx.health.services.client.data.ExerciseType HANDBALL;
+    field public static final androidx.health.services.client.data.ExerciseType HIGH_INTENSITY_INTERVAL_TRAINING;
+    field public static final androidx.health.services.client.data.ExerciseType HIKING;
+    field public static final androidx.health.services.client.data.ExerciseType HORSE_RIDING;
+    field public static final androidx.health.services.client.data.ExerciseType ICE_HOCKEY;
+    field public static final androidx.health.services.client.data.ExerciseType ICE_SKATING;
+    field public static final androidx.health.services.client.data.ExerciseType INLINE_SKATING;
+    field public static final androidx.health.services.client.data.ExerciseType JUMPING_JACK;
+    field public static final androidx.health.services.client.data.ExerciseType JUMP_ROPE;
+    field public static final androidx.health.services.client.data.ExerciseType LAT_PULL_DOWN;
+    field public static final androidx.health.services.client.data.ExerciseType LUNGE;
+    field public static final androidx.health.services.client.data.ExerciseType MARTIAL_ARTS;
+    field public static final androidx.health.services.client.data.ExerciseType MEDITATION;
+    field public static final androidx.health.services.client.data.ExerciseType MOUNTAIN_BIKING;
+    field public static final androidx.health.services.client.data.ExerciseType ORIENTEERING;
+    field public static final androidx.health.services.client.data.ExerciseType PADDLING;
+    field public static final androidx.health.services.client.data.ExerciseType PARA_GLIDING;
+    field public static final androidx.health.services.client.data.ExerciseType PILATES;
+    field public static final androidx.health.services.client.data.ExerciseType PLANK;
+    field public static final androidx.health.services.client.data.ExerciseType RACQUETBALL;
+    field public static final androidx.health.services.client.data.ExerciseType ROCK_CLIMBING;
+    field public static final androidx.health.services.client.data.ExerciseType ROLLER_HOCKEY;
+    field public static final androidx.health.services.client.data.ExerciseType ROLLER_SKATING;
+    field public static final androidx.health.services.client.data.ExerciseType ROWING;
+    field public static final androidx.health.services.client.data.ExerciseType ROWING_MACHINE;
+    field public static final androidx.health.services.client.data.ExerciseType RUGBY;
+    field public static final androidx.health.services.client.data.ExerciseType RUNNING;
+    field public static final androidx.health.services.client.data.ExerciseType RUNNING_TREADMILL;
+    field public static final androidx.health.services.client.data.ExerciseType SAILING;
+    field public static final androidx.health.services.client.data.ExerciseType SCUBA_DIVING;
+    field public static final androidx.health.services.client.data.ExerciseType SKATING;
+    field public static final androidx.health.services.client.data.ExerciseType SKIING;
+    field public static final androidx.health.services.client.data.ExerciseType SNOWBOARDING;
+    field public static final androidx.health.services.client.data.ExerciseType SNOWSHOEING;
+    field public static final androidx.health.services.client.data.ExerciseType SOCCER;
+    field public static final androidx.health.services.client.data.ExerciseType SOFTBALL;
+    field public static final androidx.health.services.client.data.ExerciseType SQUASH;
+    field public static final androidx.health.services.client.data.ExerciseType SQUAT;
+    field public static final androidx.health.services.client.data.ExerciseType STAIR_CLIMBING;
+    field public static final androidx.health.services.client.data.ExerciseType STAIR_CLIMBING_MACHINE;
+    field public static final androidx.health.services.client.data.ExerciseType STRENGTH_TRAINING;
+    field public static final androidx.health.services.client.data.ExerciseType STRETCHING;
+    field public static final androidx.health.services.client.data.ExerciseType SURFING;
+    field public static final androidx.health.services.client.data.ExerciseType SWIMMING_OPEN_WATER;
+    field public static final androidx.health.services.client.data.ExerciseType SWIMMING_POOL;
+    field public static final androidx.health.services.client.data.ExerciseType TABLE_TENNIS;
+    field public static final androidx.health.services.client.data.ExerciseType TENNIS;
+    field public static final androidx.health.services.client.data.ExerciseType UNKNOWN;
+    field public static final androidx.health.services.client.data.ExerciseType UPPER_TWIST;
+    field public static final androidx.health.services.client.data.ExerciseType VOLLEYBALL;
+    field public static final androidx.health.services.client.data.ExerciseType WALKING;
+    field public static final androidx.health.services.client.data.ExerciseType WATER_POLO;
+    field public static final androidx.health.services.client.data.ExerciseType WEIGHTLIFTING;
+    field public static final androidx.health.services.client.data.ExerciseType WORKOUT;
+    field public static final androidx.health.services.client.data.ExerciseType YACHTING;
+    field public static final androidx.health.services.client.data.ExerciseType YOGA;
+  }
+
+  public static final class ExerciseType.Companion {
+    method public androidx.health.services.client.data.ExerciseType fromId(int id);
+  }
+
+  public final class ExerciseTypeCapabilities {
+    ctor public ExerciseTypeCapabilities(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> supportedDataTypes, java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,? extends java.util.Set<androidx.health.services.client.data.ComparisonType>> supportedGoals, java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,? extends java.util.Set<androidx.health.services.client.data.ComparisonType>> supportedMilestones, boolean supportsAutoPauseAndResume);
+    method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getSupportedDataTypes();
+    method public java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,java.util.Set<androidx.health.services.client.data.ComparisonType>> getSupportedGoals();
+    method public java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,java.util.Set<androidx.health.services.client.data.ComparisonType>> getSupportedMilestones();
+    method public boolean getSupportsAutoPauseAndResume();
+    property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> supportedDataTypes;
+    property public final java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,java.util.Set<androidx.health.services.client.data.ComparisonType>> supportedGoals;
+    property public final java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,java.util.Set<androidx.health.services.client.data.ComparisonType>> supportedMilestones;
+    property public final boolean supportsAutoPauseAndResume;
+  }
+
+  public abstract class ExerciseTypeConfig {
+    field public static final androidx.health.services.client.data.ExerciseTypeConfig.Companion Companion;
+  }
+
+  public static final class ExerciseTypeConfig.Companion {
+  }
+
+  public final class ExerciseUpdate {
+    method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.IntervalDataPoint<?> dataPoint);
+    method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.SampleDataPoint<?> dataPoint);
+    method public androidx.health.services.client.data.ExerciseUpdate.ActiveDurationCheckpoint? getActiveDurationCheckpoint();
+    method public androidx.health.services.client.data.ExerciseConfig? getExerciseConfig();
+    method public androidx.health.services.client.data.ExerciseStateInfo getExerciseStateInfo();
+    method public java.util.Set<androidx.health.services.client.data.ExerciseGoal<? extends java.lang.Number>> getLatestAchievedGoals();
+    method public androidx.health.services.client.data.DataPointContainer getLatestMetrics();
+    method public java.util.Set<androidx.health.services.client.data.MilestoneMarkerSummary> getLatestMilestoneMarkerSummaries();
+    method public java.time.Instant? getStartTime();
+    method public java.time.Duration getUpdateDurationFromBoot();
+    property public final androidx.health.services.client.data.ExerciseUpdate.ActiveDurationCheckpoint? activeDurationCheckpoint;
+    property public final androidx.health.services.client.data.ExerciseConfig? exerciseConfig;
+    property public final androidx.health.services.client.data.ExerciseStateInfo exerciseStateInfo;
+    property public final java.util.Set<androidx.health.services.client.data.ExerciseGoal<? extends java.lang.Number>> latestAchievedGoals;
+    property public final androidx.health.services.client.data.DataPointContainer latestMetrics;
+    property public final java.util.Set<androidx.health.services.client.data.MilestoneMarkerSummary> latestMilestoneMarkerSummaries;
+    property public final java.time.Instant? startTime;
+    field public static final androidx.health.services.client.data.ExerciseUpdate.Companion Companion;
+  }
+
+  public static final class ExerciseUpdate.ActiveDurationCheckpoint {
+    ctor public ExerciseUpdate.ActiveDurationCheckpoint(java.time.Instant time, java.time.Duration activeDuration);
+    method public java.time.Duration getActiveDuration();
+    method public java.time.Instant getTime();
+    property public final java.time.Duration activeDuration;
+    property public final java.time.Instant time;
+  }
+
+  public static final class ExerciseUpdate.Companion {
+  }
+
+  public final class GolfExerciseTypeConfig extends androidx.health.services.client.data.ExerciseTypeConfig {
+    ctor public GolfExerciseTypeConfig(optional androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo golfShotTrackingPlaceInfo);
+    method public androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo getGolfShotTrackingPlaceInfo();
+    property public final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo golfShotTrackingPlaceInfo;
+  }
+
+  public static final class GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo {
+    method public int getPlaceInfoId();
+    property public final int placeInfoId;
+    field public static final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo.Companion Companion;
+    field public static final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo GOLF_SHOT_TRACKING_PLACE_INFO_FAIRWAY;
+    field public static final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo GOLF_SHOT_TRACKING_PLACE_INFO_PUTTING_GREEN;
+    field public static final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo GOLF_SHOT_TRACKING_PLACE_INFO_TEE_BOX;
+    field public static final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo GOLF_SHOT_TRACKING_PLACE_INFO_UNSPECIFIED;
+  }
+
+  public static final class GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo.Companion {
+  }
+
+  public final class HealthEvent {
+    ctor public HealthEvent(androidx.health.services.client.data.HealthEvent.Type type, java.time.Instant eventTime, androidx.health.services.client.data.DataPointContainer metrics);
+    method public java.time.Instant getEventTime();
+    method public androidx.health.services.client.data.DataPointContainer getMetrics();
+    method public androidx.health.services.client.data.HealthEvent.Type getType();
+    property public final java.time.Instant eventTime;
+    property public final androidx.health.services.client.data.DataPointContainer metrics;
+    property public final androidx.health.services.client.data.HealthEvent.Type type;
+  }
+
+  public static final class HealthEvent.Type {
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.HealthEvent.Type.Companion Companion;
+    field public static final androidx.health.services.client.data.HealthEvent.Type FALL_DETECTED;
+    field public static final androidx.health.services.client.data.HealthEvent.Type UNKNOWN;
+  }
+
+  public static final class HealthEvent.Type.Companion {
+  }
+
+  public final class HeartRateAccuracy extends androidx.health.services.client.data.DataPointAccuracy {
+    ctor public HeartRateAccuracy(androidx.health.services.client.data.HeartRateAccuracy.SensorStatus sensorStatus);
+    method public androidx.health.services.client.data.HeartRateAccuracy.SensorStatus getSensorStatus();
+    property public final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus sensorStatus;
+  }
+
+  public static final class HeartRateAccuracy.SensorStatus {
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus ACCURACY_HIGH;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus ACCURACY_LOW;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus ACCURACY_MEDIUM;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus.Companion Companion;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus NO_CONTACT;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus UNKNOWN;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus UNRELIABLE;
+  }
+
+  public static final class HeartRateAccuracy.SensorStatus.Companion {
+  }
+
+  public final class IntervalDataPoint<T> extends androidx.health.services.client.data.DataPoint<T> {
+    ctor public IntervalDataPoint(androidx.health.services.client.data.DataType<T,? extends androidx.health.services.client.data.IntervalDataPoint<T>> dataType, T value, java.time.Duration startDurationFromBoot, java.time.Duration endDurationFromBoot, optional android.os.Bundle metadata, optional androidx.health.services.client.data.DataPointAccuracy? accuracy);
+    method public androidx.health.services.client.data.DataPointAccuracy? getAccuracy();
+    method public androidx.health.services.client.data.DataType<T,? extends androidx.health.services.client.data.IntervalDataPoint<T>> getDataType();
+    method public java.time.Duration getEndDurationFromBoot();
+    method public java.time.Instant getEndInstant(java.time.Instant bootInstant);
+    method public android.os.Bundle getMetadata();
+    method public java.time.Duration getStartDurationFromBoot();
+    method public java.time.Instant getStartInstant(java.time.Instant bootInstant);
+    method public T getValue();
+    property public final androidx.health.services.client.data.DataPointAccuracy? accuracy;
+    property public androidx.health.services.client.data.DataType<T,? extends androidx.health.services.client.data.IntervalDataPoint<T>> dataType;
+    property public final java.time.Duration endDurationFromBoot;
+    property public final android.os.Bundle metadata;
+    property public final java.time.Duration startDurationFromBoot;
+    property public final T value;
+  }
+
+  public final class LocationAccuracy extends androidx.health.services.client.data.DataPointAccuracy {
+    ctor public LocationAccuracy(@FloatRange(from=0.0) double horizontalPositionErrorMeters, optional @FloatRange(from=0.0) double verticalPositionErrorMeters);
+    method public double getHorizontalPositionErrorMeters();
+    method public double getVerticalPositionErrorMeters();
+    property public final double horizontalPositionErrorMeters;
+    property public final double verticalPositionErrorMeters;
+    field public static final androidx.health.services.client.data.LocationAccuracy.Companion Companion;
+  }
+
+  public static final class LocationAccuracy.Companion {
+  }
+
+  public final class LocationAvailability implements androidx.health.services.client.data.Availability {
+    method public static androidx.health.services.client.data.LocationAvailability? fromId(int id);
+    method public int getId();
+    method public String getName();
+    property public int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.LocationAvailability ACQUIRED_TETHERED;
+    field public static final androidx.health.services.client.data.LocationAvailability ACQUIRED_UNTETHERED;
+    field public static final androidx.health.services.client.data.LocationAvailability ACQUIRING;
+    field public static final androidx.health.services.client.data.LocationAvailability.Companion Companion;
+    field public static final androidx.health.services.client.data.LocationAvailability NO_GNSS;
+    field public static final androidx.health.services.client.data.LocationAvailability UNAVAILABLE;
+    field public static final androidx.health.services.client.data.LocationAvailability UNKNOWN;
+  }
+
+  public static final class LocationAvailability.Companion {
+    method public androidx.health.services.client.data.LocationAvailability? fromId(int id);
+  }
+
+  public final class LocationData {
+    ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional double altitude, optional double bearing);
+    method public double getAltitude();
+    method public double getBearing();
+    method public double getLatitude();
+    method public double getLongitude();
+    property public final double altitude;
+    property public final double bearing;
+    property public final double latitude;
+    property public final double longitude;
+    field public static final double ALTITUDE_UNAVAILABLE = (0.0/0.0);
+    field public static final double BEARING_UNAVAILABLE = (0.0/0.0);
+  }
+
+  public final class MeasureCapabilities {
+    ctor public MeasureCapabilities(java.util.Set<? extends androidx.health.services.client.data.DeltaDataType<?,?>> supportedDataTypesMeasure);
+    method public java.util.Set<androidx.health.services.client.data.DeltaDataType<?,?>> getSupportedDataTypesMeasure();
+    property public final java.util.Set<androidx.health.services.client.data.DeltaDataType<?,?>> supportedDataTypesMeasure;
+  }
+
+  public final class MilestoneMarkerSummary {
+    ctor public MilestoneMarkerSummary(java.time.Instant startTime, java.time.Instant endTime, java.time.Duration activeDuration, androidx.health.services.client.data.ExerciseGoal<? extends java.lang.Number> achievedGoal, androidx.health.services.client.data.DataPointContainer summaryMetrics);
+    method public androidx.health.services.client.data.ExerciseGoal<? extends java.lang.Number> getAchievedGoal();
+    method public java.time.Duration getActiveDuration();
+    method public java.time.Instant getEndTime();
+    method public java.time.Instant getStartTime();
+    method public androidx.health.services.client.data.DataPointContainer getSummaryMetrics();
+    property public final androidx.health.services.client.data.ExerciseGoal<? extends java.lang.Number> achievedGoal;
+    property public final java.time.Duration activeDuration;
+    property public final java.time.Instant endTime;
+    property public final java.time.Instant startTime;
+    property public final androidx.health.services.client.data.DataPointContainer summaryMetrics;
+  }
+
+  public final class PassiveGoal {
+    ctor public PassiveGoal(androidx.health.services.client.data.DataTypeCondition<? extends java.lang.Number,? extends androidx.health.services.client.data.DeltaDataType<? extends java.lang.Number,?>> dataTypeCondition);
+    method public androidx.health.services.client.data.DataTypeCondition<? extends java.lang.Number,? extends androidx.health.services.client.data.DeltaDataType<? extends java.lang.Number,?>> getDataTypeCondition();
+    property public final androidx.health.services.client.data.DataTypeCondition<? extends java.lang.Number,? extends androidx.health.services.client.data.DeltaDataType<? extends java.lang.Number,?>> dataTypeCondition;
+  }
+
+  public final class PassiveListenerConfig {
+    ctor public PassiveListenerConfig(java.util.Set<? extends androidx.health.services.client.data.DataType<? extends java.lang.Object,? extends androidx.health.services.client.data.DataPoint<?>>> dataTypes, boolean shouldUserActivityInfoBeRequested, java.util.Set<androidx.health.services.client.data.PassiveGoal> dailyGoals, java.util.Set<androidx.health.services.client.data.HealthEvent.Type> healthEventTypes);
+    method public static androidx.health.services.client.data.PassiveListenerConfig.Builder builder();
+    method public java.util.Set<androidx.health.services.client.data.PassiveGoal> getDailyGoals();
+    method public java.util.Set<androidx.health.services.client.data.DataType<? extends java.lang.Object,? extends androidx.health.services.client.data.DataPoint<?>>> getDataTypes();
+    method public java.util.Set<androidx.health.services.client.data.HealthEvent.Type> getHealthEventTypes();
+    method public boolean getShouldUserActivityInfoBeRequested();
+    property public final java.util.Set<androidx.health.services.client.data.PassiveGoal> dailyGoals;
+    property public final java.util.Set<androidx.health.services.client.data.DataType<? extends java.lang.Object,? extends androidx.health.services.client.data.DataPoint<?>>> dataTypes;
+    property public final java.util.Set<androidx.health.services.client.data.HealthEvent.Type> healthEventTypes;
+    property public final boolean shouldUserActivityInfoBeRequested;
+    field public static final androidx.health.services.client.data.PassiveListenerConfig.Companion Companion;
+  }
+
+  public static final class PassiveListenerConfig.Builder {
+    ctor public PassiveListenerConfig.Builder();
+    method public androidx.health.services.client.data.PassiveListenerConfig build();
+    method public androidx.health.services.client.data.PassiveListenerConfig.Builder setDailyGoals(java.util.Set<androidx.health.services.client.data.PassiveGoal> dailyGoals);
+    method public androidx.health.services.client.data.PassiveListenerConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
+    method public androidx.health.services.client.data.PassiveListenerConfig.Builder setHealthEventTypes(java.util.Set<androidx.health.services.client.data.HealthEvent.Type> healthEventTypes);
+    method public androidx.health.services.client.data.PassiveListenerConfig.Builder setShouldUserActivityInfoBeRequested(boolean shouldUserActivityInfoBeRequested);
+  }
+
+  public static final class PassiveListenerConfig.Companion {
+    method public androidx.health.services.client.data.PassiveListenerConfig.Builder builder();
+  }
+
+  public final class PassiveMonitoringCapabilities {
+    ctor public PassiveMonitoringCapabilities(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> supportedDataTypesPassiveMonitoring, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> supportedDataTypesPassiveGoals, java.util.Set<androidx.health.services.client.data.HealthEvent.Type> supportedHealthEventTypes, java.util.Set<androidx.health.services.client.data.UserActivityState> supportedUserActivityStates);
+    method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getSupportedDataTypesPassiveGoals();
+    method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getSupportedDataTypesPassiveMonitoring();
+    method public java.util.Set<androidx.health.services.client.data.HealthEvent.Type> getSupportedHealthEventTypes();
+    method public java.util.Set<androidx.health.services.client.data.UserActivityState> getSupportedUserActivityStates();
+    property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> supportedDataTypesPassiveGoals;
+    property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> supportedDataTypesPassiveMonitoring;
+    property public final java.util.Set<androidx.health.services.client.data.HealthEvent.Type> supportedHealthEventTypes;
+    property public final java.util.Set<androidx.health.services.client.data.UserActivityState> supportedUserActivityStates;
+  }
+
+  public final class PassiveMonitoringUpdate {
+    ctor public PassiveMonitoringUpdate(androidx.health.services.client.data.DataPointContainer dataPoints, java.util.List<androidx.health.services.client.data.UserActivityInfo> userActivityInfoUpdates);
+    method public androidx.health.services.client.data.DataPointContainer getDataPoints();
+    method public java.util.List<androidx.health.services.client.data.UserActivityInfo> getUserActivityInfoUpdates();
+    property public final androidx.health.services.client.data.DataPointContainer dataPoints;
+    property public final java.util.List<androidx.health.services.client.data.UserActivityInfo> userActivityInfoUpdates;
+  }
+
+  public final class SampleDataPoint<T> extends androidx.health.services.client.data.DataPoint<T> {
+    ctor public SampleDataPoint(androidx.health.services.client.data.DataType<T,androidx.health.services.client.data.SampleDataPoint<T>> dataType, T value, java.time.Duration timeDurationFromBoot, optional android.os.Bundle metadata, optional androidx.health.services.client.data.DataPointAccuracy? accuracy);
+    method public androidx.health.services.client.data.DataPointAccuracy? getAccuracy();
+    method public androidx.health.services.client.data.DataType<T,androidx.health.services.client.data.SampleDataPoint<T>> getDataType();
+    method public android.os.Bundle getMetadata();
+    method public java.time.Duration getTimeDurationFromBoot();
+    method public java.time.Instant getTimeInstant(java.time.Instant bootInstant);
+    method public T getValue();
+    property public final androidx.health.services.client.data.DataPointAccuracy? accuracy;
+    property public androidx.health.services.client.data.DataType<T,androidx.health.services.client.data.SampleDataPoint<T>> dataType;
+    property public final android.os.Bundle metadata;
+    property public final java.time.Duration timeDurationFromBoot;
+    property public final T value;
+  }
+
+  public final class StatisticalDataPoint<T extends java.lang.Number> extends androidx.health.services.client.data.DataPoint<T> {
+    ctor public StatisticalDataPoint(androidx.health.services.client.data.AggregateDataType<T,androidx.health.services.client.data.StatisticalDataPoint<T>> dataType, T min, T max, T average, java.time.Instant start, java.time.Instant end);
+    method public T getAverage();
+    method public java.time.Instant getEnd();
+    method public T getMax();
+    method public T getMin();
+    method public java.time.Instant getStart();
+    property public final T average;
+    property public final java.time.Instant end;
+    property public final T max;
+    property public final T min;
+    property public final java.time.Instant start;
+    field public static final androidx.health.services.client.data.StatisticalDataPoint.Companion Companion;
+  }
+
+  public static final class StatisticalDataPoint.Companion {
+  }
+
+  public final class UserActivityInfo {
+    ctor public UserActivityInfo(androidx.health.services.client.data.UserActivityState userActivityState, androidx.health.services.client.data.ExerciseInfo? exerciseInfo, java.time.Instant stateChangeTime);
+    method public static androidx.health.services.client.data.UserActivityInfo createActiveExerciseState(androidx.health.services.client.data.ExerciseInfo exerciseInfo, java.time.Instant stateChangeTime);
+    method public static androidx.health.services.client.data.UserActivityInfo createAsleepState(java.time.Instant stateChangeTime);
+    method public static androidx.health.services.client.data.UserActivityInfo createPassiveActivityState(java.time.Instant stateChangeTime);
+    method public static androidx.health.services.client.data.UserActivityInfo createUnknownTypeState(java.time.Instant stateChangeTime);
+    method public androidx.health.services.client.data.ExerciseInfo? getExerciseInfo();
+    method public java.time.Instant getStateChangeTime();
+    method public androidx.health.services.client.data.UserActivityState getUserActivityState();
+    property public final androidx.health.services.client.data.ExerciseInfo? exerciseInfo;
+    property public final java.time.Instant stateChangeTime;
+    property public final androidx.health.services.client.data.UserActivityState userActivityState;
+    field public static final androidx.health.services.client.data.UserActivityInfo.Companion Companion;
+  }
+
+  public static final class UserActivityInfo.Companion {
+    method public androidx.health.services.client.data.UserActivityInfo createActiveExerciseState(androidx.health.services.client.data.ExerciseInfo exerciseInfo, java.time.Instant stateChangeTime);
+    method public androidx.health.services.client.data.UserActivityInfo createAsleepState(java.time.Instant stateChangeTime);
+    method public androidx.health.services.client.data.UserActivityInfo createPassiveActivityState(java.time.Instant stateChangeTime);
+    method public androidx.health.services.client.data.UserActivityInfo createUnknownTypeState(java.time.Instant stateChangeTime);
+  }
+
+  public final class UserActivityState {
+    ctor public UserActivityState(int id, String name);
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.UserActivityState.Companion Companion;
+    field public static final androidx.health.services.client.data.UserActivityState USER_ACTIVITY_ASLEEP;
+    field public static final androidx.health.services.client.data.UserActivityState USER_ACTIVITY_EXERCISE;
+    field public static final androidx.health.services.client.data.UserActivityState USER_ACTIVITY_PASSIVE;
+    field public static final androidx.health.services.client.data.UserActivityState USER_ACTIVITY_UNKNOWN;
+  }
+
+  public static final class UserActivityState.Companion {
+  }
+
+  public final class WarmUpConfig {
+    ctor public WarmUpConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DeltaDataType<?,?>> dataTypes);
+    method public java.util.Set<androidx.health.services.client.data.DeltaDataType<?,?>> getDataTypes();
+    method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    property public final java.util.Set<androidx.health.services.client.data.DeltaDataType<?,?>> dataTypes;
+    property public final androidx.health.services.client.data.ExerciseType exerciseType;
+  }
+
+}
+
diff --git a/health/health-services-client/api/current.ignore b/health/health-services-client/api/current.ignore
index a882151..091f290 100644
--- a/health/health-services-client/api/current.ignore
+++ b/health/health-services-client/api/current.ignore
@@ -1,3 +1,3 @@
 // Baseline format: 1.0
-AddedAbstractMethod: androidx.health.services.client.ExerciseClient#updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig):
-    Added method androidx.health.services.client.ExerciseClient.updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig)
+AddedAbstractMethod: androidx.health.services.client.ExerciseClient#overrideBatchingModesForActiveExerciseAsync(java.util.Set<androidx.health.services.client.data.BatchingMode>):
+    Added method androidx.health.services.client.ExerciseClient.overrideBatchingModesForActiveExerciseAsync(java.util.Set<androidx.health.services.client.data.BatchingMode>)
diff --git a/health/health-services-client/api/current.txt b/health/health-services-client/api/current.txt
index b985692..9d0902d 100644
--- a/health/health-services-client/api/current.txt
+++ b/health/health-services-client/api/current.txt
@@ -10,6 +10,7 @@
     method public com.google.common.util.concurrent.ListenableFuture<androidx.health.services.client.data.ExerciseInfo> getCurrentExerciseInfoAsync();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> markLapAsync();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> overrideAutoPauseAndResumeForActiveExerciseAsync(boolean enabled);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> overrideBatchingModesForActiveExerciseAsync(java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModes);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> pauseExerciseAsync();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> prepareExerciseAsync(androidx.health.services.client.data.WarmUpConfig configuration);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> removeGoalFromActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
@@ -141,6 +142,14 @@
   public static final class Availability.Companion {
   }
 
+  public final class BatchingMode {
+    field public static final androidx.health.services.client.data.BatchingMode.Companion Companion;
+    field public static final androidx.health.services.client.data.BatchingMode HEART_RATE_5_SECONDS;
+  }
+
+  public static final class BatchingMode.Companion {
+  }
+
   public final class ComparisonType {
     method public int getId();
     method public String getName();
@@ -307,20 +316,25 @@
   }
 
   public final class ExerciseCapabilities {
+    ctor public ExerciseCapabilities(java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities, optional java.util.Set<androidx.health.services.client.data.BatchingMode> supportedBatchingModeOverrides);
     ctor public ExerciseCapabilities(java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities);
     method public java.util.Set<androidx.health.services.client.data.ExerciseType> getAutoPauseAndResumeEnabledExercises();
     method public androidx.health.services.client.data.ExerciseTypeCapabilities getExerciseTypeCapabilities(androidx.health.services.client.data.ExerciseType exercise);
+    method public java.util.Set<androidx.health.services.client.data.BatchingMode> getSupportedBatchingModeOverrides();
     method public java.util.Set<androidx.health.services.client.data.ExerciseType> getSupportedExerciseTypes();
     method public java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> getTypeToCapabilities();
     property public final java.util.Set<androidx.health.services.client.data.ExerciseType> autoPauseAndResumeEnabledExercises;
+    property public final java.util.Set<androidx.health.services.client.data.BatchingMode> supportedBatchingModeOverrides;
     property public final java.util.Set<androidx.health.services.client.data.ExerciseType> supportedExerciseTypes;
     property public final java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities;
   }
 
   public final class ExerciseConfig {
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig, optional java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides);
     ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
     ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
     method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
+    method public java.util.Set<androidx.health.services.client.data.BatchingMode> getBatchingModeOverrides();
     method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
     method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
     method public android.os.Bundle getExerciseParams();
@@ -329,6 +343,7 @@
     method public float getSwimmingPoolLengthMeters();
     method public boolean isAutoPauseAndResumeEnabled();
     method public boolean isGpsEnabled();
+    property public final java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides;
     property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> dataTypes;
     property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
     property public final android.os.Bundle exerciseParams;
@@ -344,6 +359,7 @@
   public static final class ExerciseConfig.Builder {
     ctor public ExerciseConfig.Builder(androidx.health.services.client.data.ExerciseType exerciseType);
     method public androidx.health.services.client.data.ExerciseConfig build();
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setBatchingModeOverrides(java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
diff --git a/health/health-services-client/api/public_plus_experimental_1.0.0-beta03.txt b/health/health-services-client/api/public_plus_experimental_1.0.0-beta03.txt
new file mode 100644
index 0000000..9d0902d
--- /dev/null
+++ b/health/health-services-client/api/public_plus_experimental_1.0.0-beta03.txt
@@ -0,0 +1,909 @@
+// Signature format: 4.0
+package androidx.health.services.client {
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface ExerciseClient {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> addGoalToActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback callback);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> endExerciseAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> flushAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.health.services.client.data.ExerciseCapabilities> getCapabilitiesAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.health.services.client.data.ExerciseInfo> getCurrentExerciseInfoAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> markLapAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> overrideAutoPauseAndResumeForActiveExerciseAsync(boolean enabled);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> overrideBatchingModesForActiveExerciseAsync(java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModes);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> pauseExerciseAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> prepareExerciseAsync(androidx.health.services.client.data.WarmUpConfig configuration);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> removeGoalFromActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> resumeExerciseAsync();
+    method public void setUpdateCallback(androidx.health.services.client.ExerciseUpdateCallback callback);
+    method public void setUpdateCallback(java.util.concurrent.Executor executor, androidx.health.services.client.ExerciseUpdateCallback callback);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig);
+  }
+
+  public final class ExerciseClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? addGoalToActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? clearUpdateCallback(androidx.health.services.client.ExerciseClient, androidx.health.services.client.ExerciseUpdateCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? endExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? flush(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? getCapabilities(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseCapabilities>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? getCurrentExerciseInfo(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseInfo>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? markLap(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? overrideAutoPauseAndResumeForActiveExercise(androidx.health.services.client.ExerciseClient, boolean enabled, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? pauseExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? prepareExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.WarmUpConfig configuration, kotlin.coroutines.Continuation<? super kotlin.Unit>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super kotlin.Unit>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? updateExerciseTypeConfig(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+  }
+
+  public interface ExerciseUpdateCallback {
+    method public void onAvailabilityChanged(androidx.health.services.client.data.DataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
+    method public void onExerciseUpdateReceived(androidx.health.services.client.data.ExerciseUpdate update);
+    method public void onLapSummaryReceived(androidx.health.services.client.data.ExerciseLapSummary lapSummary);
+    method public void onRegistered();
+    method public void onRegistrationFailed(Throwable throwable);
+  }
+
+  public final class HealthServices {
+    method public static androidx.health.services.client.HealthServicesClient getClient(android.content.Context context);
+    field public static final androidx.health.services.client.HealthServices INSTANCE;
+  }
+
+  public interface HealthServicesClient {
+    method public androidx.health.services.client.ExerciseClient getExerciseClient();
+    method public androidx.health.services.client.MeasureClient getMeasureClient();
+    method public androidx.health.services.client.PassiveMonitoringClient getPassiveMonitoringClient();
+    property public abstract androidx.health.services.client.ExerciseClient exerciseClient;
+    property public abstract androidx.health.services.client.MeasureClient measureClient;
+    property public abstract androidx.health.services.client.PassiveMonitoringClient passiveMonitoringClient;
+  }
+
+  public final class HealthServicesException extends java.lang.Exception {
+    ctor public HealthServicesException(String message);
+  }
+
+  public final class ListenableFutureExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend <T> Object? awaitWithException(com.google.common.util.concurrent.ListenableFuture<T>, kotlin.coroutines.Continuation<? super T>) throws androidx.health.services.client.HealthServicesException;
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureCallback {
+    method public void onAvailabilityChanged(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
+    method public void onDataReceived(androidx.health.services.client.data.DataPointContainer data);
+    method public default void onRegistered();
+    method public default void onRegistrationFailed(Throwable throwable);
+  }
+
+  public interface MeasureClient {
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.health.services.client.data.MeasureCapabilities> getCapabilitiesAsync();
+    method public void registerMeasureCallback(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
+    method public void registerMeasureCallback(androidx.health.services.client.data.DeltaDataType<?,?> dataType, java.util.concurrent.Executor executor, androidx.health.services.client.MeasureCallback callback);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> unregisterMeasureCallbackAsync(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
+  }
+
+  public final class MeasureClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? getCapabilities(androidx.health.services.client.MeasureClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.MeasureCapabilities>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface PassiveListenerCallback {
+    method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
+    method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
+    method public default void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
+    method public default void onPermissionLost();
+    method public default void onRegistered();
+    method public default void onRegistrationFailed(Throwable throwable);
+    method public default void onUserActivityInfoReceived(androidx.health.services.client.data.UserActivityInfo info);
+  }
+
+  public abstract class PassiveListenerService extends android.app.Service {
+    ctor public PassiveListenerService();
+    method public final android.os.IBinder? onBind(android.content.Intent intent);
+    method public void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
+    method public void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
+    method public void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
+    method public void onPermissionLost();
+    method public void onUserActivityInfoReceived(androidx.health.services.client.data.UserActivityInfo info);
+  }
+
+  public interface PassiveMonitoringClient {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearPassiveListenerCallbackAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearPassiveListenerServiceAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> flushAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.health.services.client.data.PassiveMonitoringCapabilities> getCapabilitiesAsync();
+    method public void setPassiveListenerCallback(androidx.health.services.client.data.PassiveListenerConfig config, androidx.health.services.client.PassiveListenerCallback callback);
+    method public void setPassiveListenerCallback(androidx.health.services.client.data.PassiveListenerConfig config, java.util.concurrent.Executor executor, androidx.health.services.client.PassiveListenerCallback callback);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
+  }
+
+  public final class PassiveMonitoringClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? clearPassiveListenerCallback(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? clearPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? flush(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? getCapabilities(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.PassiveMonitoringCapabilities>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? setPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+  }
+
+}
+
+package androidx.health.services.client.data {
+
+  public final class AggregateDataType<T extends java.lang.Number, D extends androidx.health.services.client.data.DataPoint<T>> extends androidx.health.services.client.data.DataType<T,D> {
+    ctor public AggregateDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface Availability {
+    method public int getId();
+    property public abstract int id;
+    field public static final androidx.health.services.client.data.Availability.Companion Companion;
+  }
+
+  public static final class Availability.Companion {
+  }
+
+  public final class BatchingMode {
+    field public static final androidx.health.services.client.data.BatchingMode.Companion Companion;
+    field public static final androidx.health.services.client.data.BatchingMode HEART_RATE_5_SECONDS;
+  }
+
+  public static final class BatchingMode.Companion {
+  }
+
+  public final class ComparisonType {
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.ComparisonType.Companion Companion;
+    field public static final androidx.health.services.client.data.ComparisonType GREATER_THAN;
+    field public static final androidx.health.services.client.data.ComparisonType GREATER_THAN_OR_EQUAL;
+    field public static final androidx.health.services.client.data.ComparisonType LESS_THAN;
+    field public static final androidx.health.services.client.data.ComparisonType LESS_THAN_OR_EQUAL;
+    field public static final androidx.health.services.client.data.ComparisonType UNKNOWN;
+  }
+
+  public static final class ComparisonType.Companion {
+  }
+
+  public final class CumulativeDataPoint<T extends java.lang.Number> extends androidx.health.services.client.data.DataPoint<T> {
+    ctor public CumulativeDataPoint(androidx.health.services.client.data.AggregateDataType<T,androidx.health.services.client.data.CumulativeDataPoint<T>> dataType, T total, java.time.Instant start, java.time.Instant end);
+    method public java.time.Instant getEnd();
+    method public java.time.Instant getStart();
+    method public T getTotal();
+    property public final java.time.Instant end;
+    property public final java.time.Instant start;
+    property public final T total;
+  }
+
+  public abstract class DataPoint<T> {
+    method public androidx.health.services.client.data.DataType<T,? extends androidx.health.services.client.data.DataPoint<T>> getDataType();
+    property public androidx.health.services.client.data.DataType<T,? extends androidx.health.services.client.data.DataPoint<T>> dataType;
+  }
+
+  public abstract class DataPointAccuracy {
+    ctor public DataPointAccuracy();
+  }
+
+  public final class DataPointContainer {
+    ctor public DataPointContainer(java.util.Map<androidx.health.services.client.data.DataType<?,?>,? extends java.util.List<? extends androidx.health.services.client.data.DataPoint<?>>> dataPoints);
+    ctor public DataPointContainer(java.util.List<? extends androidx.health.services.client.data.DataPoint<?>> dataPointList);
+    method public java.util.List<androidx.health.services.client.data.CumulativeDataPoint<?>> getCumulativeDataPoints();
+    method public <T, D extends androidx.health.services.client.data.DataPoint<T>> java.util.List<D> getData(androidx.health.services.client.data.DeltaDataType<T,D> type);
+    method public <T extends java.lang.Number, D extends androidx.health.services.client.data.DataPoint<T>> D? getData(androidx.health.services.client.data.AggregateDataType<T,D> type);
+    method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
+    method public java.util.List<androidx.health.services.client.data.IntervalDataPoint<?>> getIntervalDataPoints();
+    method public java.util.List<androidx.health.services.client.data.SampleDataPoint<?>> getSampleDataPoints();
+    method public java.util.List<androidx.health.services.client.data.StatisticalDataPoint<?>> getStatisticalDataPoints();
+    property public final java.util.List<androidx.health.services.client.data.CumulativeDataPoint<?>> cumulativeDataPoints;
+    property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> dataTypes;
+    property public final java.util.List<androidx.health.services.client.data.IntervalDataPoint<?>> intervalDataPoints;
+    property public final java.util.List<androidx.health.services.client.data.SampleDataPoint<?>> sampleDataPoints;
+    property public final java.util.List<androidx.health.services.client.data.StatisticalDataPoint<?>> statisticalDataPoints;
+  }
+
+  public abstract class DataType<T, D extends androidx.health.services.client.data.DataPoint<T>> {
+    ctor public DataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass, boolean isAggregate);
+    method public final String getName();
+    method public final Class<T> getValueClass();
+    property public final String name;
+    property public final Class<T> valueClass;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> ABSOLUTE_ELEVATION;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> ABSOLUTE_ELEVATION_STATS;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> ACTIVE_EXERCISE_DURATION_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> CALORIES;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> CALORIES_DAILY;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> CALORIES_TOTAL;
+    field public static final androidx.health.services.client.data.DataType.Companion Companion;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> DECLINE_DISTANCE;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> DECLINE_DISTANCE_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> DECLINE_DURATION;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> DECLINE_DURATION_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> DISTANCE;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> DISTANCE_DAILY;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> DISTANCE_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> ELEVATION_GAIN;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> ELEVATION_GAIN_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> ELEVATION_LOSS;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> ELEVATION_LOSS_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> FLAT_GROUND_DISTANCE;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> FLAT_GROUND_DISTANCE_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> FLAT_GROUND_DURATION;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> FLAT_GROUND_DURATION_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> FLOORS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> FLOORS_DAILY;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> FLOORS_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> GOLF_SHOT_COUNT;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> GOLF_SHOT_COUNT_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> HEART_RATE_BPM;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> HEART_RATE_BPM_STATS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> INCLINE_DISTANCE;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> INCLINE_DISTANCE_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> INCLINE_DURATION;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> INCLINE_DURATION_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<androidx.health.services.client.data.LocationData,androidx.health.services.client.data.SampleDataPoint<androidx.health.services.client.data.LocationData>> LOCATION;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> PACE;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> PACE_STATS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> REP_COUNT;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> REP_COUNT_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> RESTING_EXERCISE_DURATION;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> RESTING_EXERCISE_DURATION_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> RUNNING_STEPS;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> RUNNING_STEPS_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> SPEED;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> SPEED_STATS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> STEPS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> STEPS_DAILY;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.SampleDataPoint<java.lang.Long>> STEPS_PER_MINUTE;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Long>> STEPS_PER_MINUTE_STATS;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> STEPS_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> SWIMMING_LAP_COUNT;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> SWIMMING_STROKES;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> SWIMMING_STROKES_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> VO2_MAX;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> VO2_MAX_STATS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> WALKING_STEPS;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> WALKING_STEPS_TOTAL;
+  }
+
+  public static final class DataType.Companion {
+  }
+
+  public static final class DataType.TimeType {
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.DataType.TimeType.Companion Companion;
+    field public static final androidx.health.services.client.data.DataType.TimeType INTERVAL;
+    field public static final androidx.health.services.client.data.DataType.TimeType SAMPLE;
+    field public static final androidx.health.services.client.data.DataType.TimeType UNKNOWN;
+  }
+
+  public static final class DataType.TimeType.Companion {
+  }
+
+  public final class DataTypeAvailability implements androidx.health.services.client.data.Availability {
+    method public static androidx.health.services.client.data.DataTypeAvailability? fromId(int id);
+    method public int getId();
+    method public String getName();
+    property public int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.DataTypeAvailability ACQUIRING;
+    field public static final androidx.health.services.client.data.DataTypeAvailability AVAILABLE;
+    field public static final androidx.health.services.client.data.DataTypeAvailability.Companion Companion;
+    field public static final androidx.health.services.client.data.DataTypeAvailability UNAVAILABLE;
+    field public static final androidx.health.services.client.data.DataTypeAvailability UNAVAILABLE_DEVICE_OFF_BODY;
+    field public static final androidx.health.services.client.data.DataTypeAvailability UNKNOWN;
+  }
+
+  public static final class DataTypeAvailability.Companion {
+    method public androidx.health.services.client.data.DataTypeAvailability? fromId(int id);
+  }
+
+  public final class DataTypeCondition<T extends java.lang.Number, D extends androidx.health.services.client.data.DataType<T, ? extends androidx.health.services.client.data.DataPoint<T>>> {
+    ctor public DataTypeCondition(D dataType, T threshold, androidx.health.services.client.data.ComparisonType comparisonType);
+    method public androidx.health.services.client.data.ComparisonType getComparisonType();
+    method public D getDataType();
+    method public T getThreshold();
+    property public final androidx.health.services.client.data.ComparisonType comparisonType;
+    property public final D dataType;
+    property public final T threshold;
+  }
+
+  public final class DeltaDataType<T, D extends androidx.health.services.client.data.DataPoint<T>> extends androidx.health.services.client.data.DataType<T,D> {
+    ctor public DeltaDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
+  }
+
+  public final class ExerciseCapabilities {
+    ctor public ExerciseCapabilities(java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities, optional java.util.Set<androidx.health.services.client.data.BatchingMode> supportedBatchingModeOverrides);
+    ctor public ExerciseCapabilities(java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities);
+    method public java.util.Set<androidx.health.services.client.data.ExerciseType> getAutoPauseAndResumeEnabledExercises();
+    method public androidx.health.services.client.data.ExerciseTypeCapabilities getExerciseTypeCapabilities(androidx.health.services.client.data.ExerciseType exercise);
+    method public java.util.Set<androidx.health.services.client.data.BatchingMode> getSupportedBatchingModeOverrides();
+    method public java.util.Set<androidx.health.services.client.data.ExerciseType> getSupportedExerciseTypes();
+    method public java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> getTypeToCapabilities();
+    property public final java.util.Set<androidx.health.services.client.data.ExerciseType> autoPauseAndResumeEnabledExercises;
+    property public final java.util.Set<androidx.health.services.client.data.BatchingMode> supportedBatchingModeOverrides;
+    property public final java.util.Set<androidx.health.services.client.data.ExerciseType> supportedExerciseTypes;
+    property public final java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities;
+  }
+
+  public final class ExerciseConfig {
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig, optional java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
+    method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
+    method public java.util.Set<androidx.health.services.client.data.BatchingMode> getBatchingModeOverrides();
+    method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
+    method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
+    method public android.os.Bundle getExerciseParams();
+    method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    method public androidx.health.services.client.data.ExerciseTypeConfig? getExerciseTypeConfig();
+    method public float getSwimmingPoolLengthMeters();
+    method public boolean isAutoPauseAndResumeEnabled();
+    method public boolean isGpsEnabled();
+    property public final java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides;
+    property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> dataTypes;
+    property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
+    property public final android.os.Bundle exerciseParams;
+    property public final androidx.health.services.client.data.ExerciseType exerciseType;
+    property public final androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig;
+    property public final boolean isAutoPauseAndResumeEnabled;
+    property public final boolean isGpsEnabled;
+    property public final float swimmingPoolLengthMeters;
+    field public static final androidx.health.services.client.data.ExerciseConfig.Companion Companion;
+    field public static final float SWIMMING_POOL_LENGTH_UNSPECIFIED = 0.0f;
+  }
+
+  public static final class ExerciseConfig.Builder {
+    ctor public ExerciseConfig.Builder(androidx.health.services.client.data.ExerciseType exerciseType);
+    method public androidx.health.services.client.data.ExerciseConfig build();
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setBatchingModeOverrides(java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseTypeConfig(androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLengthMeters(float swimmingPoolLength);
+  }
+
+  public static final class ExerciseConfig.Companion {
+    method public androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
+  }
+
+  public final class ExerciseGoal<T extends java.lang.Number> implements android.os.Parcelable {
+    method public static <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createMilestone(androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> condition, T period);
+    method public static <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createMilestoneGoalWithUpdatedThreshold(androidx.health.services.client.data.ExerciseGoal<T> goal, T newThreshold);
+    method public static <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createOneTimeGoal(androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> condition);
+    method public int describeContents();
+    method public androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> getDataTypeCondition();
+    method public androidx.health.services.client.data.ExerciseGoalType getExerciseGoalType();
+    method public T? getPeriod();
+    method public void writeToParcel(android.os.Parcel dest, int flags);
+    property public final androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> dataTypeCondition;
+    property public final androidx.health.services.client.data.ExerciseGoalType exerciseGoalType;
+    property public final T? period;
+    field public static final android.os.Parcelable.Creator<androidx.health.services.client.data.ExerciseGoal<?>> CREATOR;
+    field public static final androidx.health.services.client.data.ExerciseGoal.Companion Companion;
+  }
+
+  public static final class ExerciseGoal.Companion {
+    method public <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createMilestone(androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> condition, T period);
+    method public <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createMilestoneGoalWithUpdatedThreshold(androidx.health.services.client.data.ExerciseGoal<T> goal, T newThreshold);
+    method public <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createOneTimeGoal(androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> condition);
+  }
+
+  public final class ExerciseGoalType {
+    method public static androidx.health.services.client.data.ExerciseGoalType? fromId(int id);
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.ExerciseGoalType.Companion Companion;
+    field public static final androidx.health.services.client.data.ExerciseGoalType MILESTONE;
+    field public static final androidx.health.services.client.data.ExerciseGoalType ONE_TIME_GOAL;
+  }
+
+  public static final class ExerciseGoalType.Companion {
+    method public androidx.health.services.client.data.ExerciseGoalType? fromId(int id);
+  }
+
+  public final class ExerciseInfo {
+    ctor public ExerciseInfo(int exerciseTrackedStatus, androidx.health.services.client.data.ExerciseType exerciseType);
+    method public int getExerciseTrackedStatus();
+    method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    property public final int exerciseTrackedStatus;
+    property public final androidx.health.services.client.data.ExerciseType exerciseType;
+  }
+
+  public final class ExerciseLapSummary {
+    ctor public ExerciseLapSummary(int lapCount, java.time.Instant startTime, java.time.Instant endTime, java.time.Duration activeDuration, androidx.health.services.client.data.DataPointContainer lapMetrics);
+    method public java.time.Duration getActiveDuration();
+    method public java.time.Instant getEndTime();
+    method public int getLapCount();
+    method public androidx.health.services.client.data.DataPointContainer getLapMetrics();
+    method public java.time.Instant getStartTime();
+    property public final java.time.Duration activeDuration;
+    property public final java.time.Instant endTime;
+    property public final int lapCount;
+    property public final androidx.health.services.client.data.DataPointContainer lapMetrics;
+    property public final java.time.Instant startTime;
+  }
+
+  public final class ExerciseState {
+    method public static androidx.health.services.client.data.ExerciseState? fromId(int id);
+    method public int getId();
+    method public String getName();
+    method public boolean isEnded();
+    method public boolean isEnding();
+    method public boolean isPaused();
+    method public boolean isResuming();
+    property public final int id;
+    property public final boolean isEnded;
+    property public final boolean isEnding;
+    property public final boolean isPaused;
+    property public final boolean isResuming;
+    property public final String name;
+    field public static final androidx.health.services.client.data.ExerciseState ACTIVE;
+    field public static final androidx.health.services.client.data.ExerciseState AUTO_PAUSED;
+    field public static final androidx.health.services.client.data.ExerciseState AUTO_PAUSING;
+    field public static final androidx.health.services.client.data.ExerciseState AUTO_RESUMING;
+    field public static final androidx.health.services.client.data.ExerciseState.Companion Companion;
+    field public static final androidx.health.services.client.data.ExerciseState ENDED;
+    field public static final androidx.health.services.client.data.ExerciseState ENDING;
+    field public static final androidx.health.services.client.data.ExerciseState PREPARING;
+    field public static final androidx.health.services.client.data.ExerciseState USER_PAUSED;
+    field public static final androidx.health.services.client.data.ExerciseState USER_PAUSING;
+    field public static final androidx.health.services.client.data.ExerciseState USER_RESUMING;
+    field public static final androidx.health.services.client.data.ExerciseState USER_STARTING;
+  }
+
+  public static final class ExerciseState.Companion {
+    method public androidx.health.services.client.data.ExerciseState? fromId(int id);
+  }
+
+  public final class ExerciseStateInfo {
+    ctor public ExerciseStateInfo(androidx.health.services.client.data.ExerciseState exerciseState, int exerciseEndReason);
+    method public int getEndReason();
+    method public androidx.health.services.client.data.ExerciseState getState();
+    property public final int endReason;
+    property public final androidx.health.services.client.data.ExerciseState state;
+    field public static final androidx.health.services.client.data.ExerciseStateInfo.Companion Companion;
+  }
+
+  public static final class ExerciseStateInfo.Companion {
+  }
+
+  public final class ExerciseType {
+    method public static androidx.health.services.client.data.ExerciseType fromId(int id);
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.ExerciseType ALPINE_SKIING;
+    field public static final androidx.health.services.client.data.ExerciseType BACKPACKING;
+    field public static final androidx.health.services.client.data.ExerciseType BACK_EXTENSION;
+    field public static final androidx.health.services.client.data.ExerciseType BADMINTON;
+    field public static final androidx.health.services.client.data.ExerciseType BARBELL_SHOULDER_PRESS;
+    field public static final androidx.health.services.client.data.ExerciseType BASEBALL;
+    field public static final androidx.health.services.client.data.ExerciseType BASKETBALL;
+    field public static final androidx.health.services.client.data.ExerciseType BENCH_PRESS;
+    field public static final androidx.health.services.client.data.ExerciseType BIKING;
+    field public static final androidx.health.services.client.data.ExerciseType BIKING_STATIONARY;
+    field public static final androidx.health.services.client.data.ExerciseType BOOT_CAMP;
+    field public static final androidx.health.services.client.data.ExerciseType BOXING;
+    field public static final androidx.health.services.client.data.ExerciseType BURPEE;
+    field public static final androidx.health.services.client.data.ExerciseType CALISTHENICS;
+    field public static final androidx.health.services.client.data.ExerciseType CRICKET;
+    field public static final androidx.health.services.client.data.ExerciseType CROSS_COUNTRY_SKIING;
+    field public static final androidx.health.services.client.data.ExerciseType CRUNCH;
+    field public static final androidx.health.services.client.data.ExerciseType.Companion Companion;
+    field public static final androidx.health.services.client.data.ExerciseType DANCING;
+    field public static final androidx.health.services.client.data.ExerciseType DEADLIFT;
+    field public static final androidx.health.services.client.data.ExerciseType ELLIPTICAL;
+    field public static final androidx.health.services.client.data.ExerciseType EXERCISE_CLASS;
+    field public static final androidx.health.services.client.data.ExerciseType FENCING;
+    field public static final androidx.health.services.client.data.ExerciseType FOOTBALL_AMERICAN;
+    field public static final androidx.health.services.client.data.ExerciseType FOOTBALL_AUSTRALIAN;
+    field public static final androidx.health.services.client.data.ExerciseType FORWARD_TWIST;
+    field public static final androidx.health.services.client.data.ExerciseType FRISBEE_DISC;
+    field public static final androidx.health.services.client.data.ExerciseType GOLF;
+    field public static final androidx.health.services.client.data.ExerciseType GUIDED_BREATHING;
+    field public static final androidx.health.services.client.data.ExerciseType GYMNASTICS;
+    field public static final androidx.health.services.client.data.ExerciseType HANDBALL;
+    field public static final androidx.health.services.client.data.ExerciseType HIGH_INTENSITY_INTERVAL_TRAINING;
+    field public static final androidx.health.services.client.data.ExerciseType HIKING;
+    field public static final androidx.health.services.client.data.ExerciseType HORSE_RIDING;
+    field public static final androidx.health.services.client.data.ExerciseType ICE_HOCKEY;
+    field public static final androidx.health.services.client.data.ExerciseType ICE_SKATING;
+    field public static final androidx.health.services.client.data.ExerciseType INLINE_SKATING;
+    field public static final androidx.health.services.client.data.ExerciseType JUMPING_JACK;
+    field public static final androidx.health.services.client.data.ExerciseType JUMP_ROPE;
+    field public static final androidx.health.services.client.data.ExerciseType LAT_PULL_DOWN;
+    field public static final androidx.health.services.client.data.ExerciseType LUNGE;
+    field public static final androidx.health.services.client.data.ExerciseType MARTIAL_ARTS;
+    field public static final androidx.health.services.client.data.ExerciseType MEDITATION;
+    field public static final androidx.health.services.client.data.ExerciseType MOUNTAIN_BIKING;
+    field public static final androidx.health.services.client.data.ExerciseType ORIENTEERING;
+    field public static final androidx.health.services.client.data.ExerciseType PADDLING;
+    field public static final androidx.health.services.client.data.ExerciseType PARA_GLIDING;
+    field public static final androidx.health.services.client.data.ExerciseType PILATES;
+    field public static final androidx.health.services.client.data.ExerciseType PLANK;
+    field public static final androidx.health.services.client.data.ExerciseType RACQUETBALL;
+    field public static final androidx.health.services.client.data.ExerciseType ROCK_CLIMBING;
+    field public static final androidx.health.services.client.data.ExerciseType ROLLER_HOCKEY;
+    field public static final androidx.health.services.client.data.ExerciseType ROLLER_SKATING;
+    field public static final androidx.health.services.client.data.ExerciseType ROWING;
+    field public static final androidx.health.services.client.data.ExerciseType ROWING_MACHINE;
+    field public static final androidx.health.services.client.data.ExerciseType RUGBY;
+    field public static final androidx.health.services.client.data.ExerciseType RUNNING;
+    field public static final androidx.health.services.client.data.ExerciseType RUNNING_TREADMILL;
+    field public static final androidx.health.services.client.data.ExerciseType SAILING;
+    field public static final androidx.health.services.client.data.ExerciseType SCUBA_DIVING;
+    field public static final androidx.health.services.client.data.ExerciseType SKATING;
+    field public static final androidx.health.services.client.data.ExerciseType SKIING;
+    field public static final androidx.health.services.client.data.ExerciseType SNOWBOARDING;
+    field public static final androidx.health.services.client.data.ExerciseType SNOWSHOEING;
+    field public static final androidx.health.services.client.data.ExerciseType SOCCER;
+    field public static final androidx.health.services.client.data.ExerciseType SOFTBALL;
+    field public static final androidx.health.services.client.data.ExerciseType SQUASH;
+    field public static final androidx.health.services.client.data.ExerciseType SQUAT;
+    field public static final androidx.health.services.client.data.ExerciseType STAIR_CLIMBING;
+    field public static final androidx.health.services.client.data.ExerciseType STAIR_CLIMBING_MACHINE;
+    field public static final androidx.health.services.client.data.ExerciseType STRENGTH_TRAINING;
+    field public static final androidx.health.services.client.data.ExerciseType STRETCHING;
+    field public static final androidx.health.services.client.data.ExerciseType SURFING;
+    field public static final androidx.health.services.client.data.ExerciseType SWIMMING_OPEN_WATER;
+    field public static final androidx.health.services.client.data.ExerciseType SWIMMING_POOL;
+    field public static final androidx.health.services.client.data.ExerciseType TABLE_TENNIS;
+    field public static final androidx.health.services.client.data.ExerciseType TENNIS;
+    field public static final androidx.health.services.client.data.ExerciseType UNKNOWN;
+    field public static final androidx.health.services.client.data.ExerciseType UPPER_TWIST;
+    field public static final androidx.health.services.client.data.ExerciseType VOLLEYBALL;
+    field public static final androidx.health.services.client.data.ExerciseType WALKING;
+    field public static final androidx.health.services.client.data.ExerciseType WATER_POLO;
+    field public static final androidx.health.services.client.data.ExerciseType WEIGHTLIFTING;
+    field public static final androidx.health.services.client.data.ExerciseType WORKOUT;
+    field public static final androidx.health.services.client.data.ExerciseType YACHTING;
+    field public static final androidx.health.services.client.data.ExerciseType YOGA;
+  }
+
+  public static final class ExerciseType.Companion {
+    method public androidx.health.services.client.data.ExerciseType fromId(int id);
+  }
+
+  public final class ExerciseTypeCapabilities {
+    ctor public ExerciseTypeCapabilities(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> supportedDataTypes, java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,? extends java.util.Set<androidx.health.services.client.data.ComparisonType>> supportedGoals, java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,? extends java.util.Set<androidx.health.services.client.data.ComparisonType>> supportedMilestones, boolean supportsAutoPauseAndResume);
+    method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getSupportedDataTypes();
+    method public java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,java.util.Set<androidx.health.services.client.data.ComparisonType>> getSupportedGoals();
+    method public java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,java.util.Set<androidx.health.services.client.data.ComparisonType>> getSupportedMilestones();
+    method public boolean getSupportsAutoPauseAndResume();
+    property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> supportedDataTypes;
+    property public final java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,java.util.Set<androidx.health.services.client.data.ComparisonType>> supportedGoals;
+    property public final java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,java.util.Set<androidx.health.services.client.data.ComparisonType>> supportedMilestones;
+    property public final boolean supportsAutoPauseAndResume;
+  }
+
+  public abstract class ExerciseTypeConfig {
+    field public static final androidx.health.services.client.data.ExerciseTypeConfig.Companion Companion;
+  }
+
+  public static final class ExerciseTypeConfig.Companion {
+  }
+
+  public final class ExerciseUpdate {
+    method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.IntervalDataPoint<?> dataPoint);
+    method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.SampleDataPoint<?> dataPoint);
+    method public androidx.health.services.client.data.ExerciseUpdate.ActiveDurationCheckpoint? getActiveDurationCheckpoint();
+    method public androidx.health.services.client.data.ExerciseConfig? getExerciseConfig();
+    method public androidx.health.services.client.data.ExerciseStateInfo getExerciseStateInfo();
+    method public java.util.Set<androidx.health.services.client.data.ExerciseGoal<? extends java.lang.Number>> getLatestAchievedGoals();
+    method public androidx.health.services.client.data.DataPointContainer getLatestMetrics();
+    method public java.util.Set<androidx.health.services.client.data.MilestoneMarkerSummary> getLatestMilestoneMarkerSummaries();
+    method public java.time.Instant? getStartTime();
+    method public java.time.Duration getUpdateDurationFromBoot();
+    property public final androidx.health.services.client.data.ExerciseUpdate.ActiveDurationCheckpoint? activeDurationCheckpoint;
+    property public final androidx.health.services.client.data.ExerciseConfig? exerciseConfig;
+    property public final androidx.health.services.client.data.ExerciseStateInfo exerciseStateInfo;
+    property public final java.util.Set<androidx.health.services.client.data.ExerciseGoal<? extends java.lang.Number>> latestAchievedGoals;
+    property public final androidx.health.services.client.data.DataPointContainer latestMetrics;
+    property public final java.util.Set<androidx.health.services.client.data.MilestoneMarkerSummary> latestMilestoneMarkerSummaries;
+    property public final java.time.Instant? startTime;
+    field public static final androidx.health.services.client.data.ExerciseUpdate.Companion Companion;
+  }
+
+  public static final class ExerciseUpdate.ActiveDurationCheckpoint {
+    ctor public ExerciseUpdate.ActiveDurationCheckpoint(java.time.Instant time, java.time.Duration activeDuration);
+    method public java.time.Duration getActiveDuration();
+    method public java.time.Instant getTime();
+    property public final java.time.Duration activeDuration;
+    property public final java.time.Instant time;
+  }
+
+  public static final class ExerciseUpdate.Companion {
+  }
+
+  public final class GolfExerciseTypeConfig extends androidx.health.services.client.data.ExerciseTypeConfig {
+    ctor public GolfExerciseTypeConfig(optional androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo golfShotTrackingPlaceInfo);
+    method public androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo getGolfShotTrackingPlaceInfo();
+    property public final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo golfShotTrackingPlaceInfo;
+  }
+
+  public static final class GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo {
+    method public int getPlaceInfoId();
+    property public final int placeInfoId;
+    field public static final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo.Companion Companion;
+    field public static final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo GOLF_SHOT_TRACKING_PLACE_INFO_FAIRWAY;
+    field public static final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo GOLF_SHOT_TRACKING_PLACE_INFO_PUTTING_GREEN;
+    field public static final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo GOLF_SHOT_TRACKING_PLACE_INFO_TEE_BOX;
+    field public static final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo GOLF_SHOT_TRACKING_PLACE_INFO_UNSPECIFIED;
+  }
+
+  public static final class GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo.Companion {
+  }
+
+  public final class HealthEvent {
+    ctor public HealthEvent(androidx.health.services.client.data.HealthEvent.Type type, java.time.Instant eventTime, androidx.health.services.client.data.DataPointContainer metrics);
+    method public java.time.Instant getEventTime();
+    method public androidx.health.services.client.data.DataPointContainer getMetrics();
+    method public androidx.health.services.client.data.HealthEvent.Type getType();
+    property public final java.time.Instant eventTime;
+    property public final androidx.health.services.client.data.DataPointContainer metrics;
+    property public final androidx.health.services.client.data.HealthEvent.Type type;
+  }
+
+  public static final class HealthEvent.Type {
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.HealthEvent.Type.Companion Companion;
+    field public static final androidx.health.services.client.data.HealthEvent.Type FALL_DETECTED;
+    field public static final androidx.health.services.client.data.HealthEvent.Type UNKNOWN;
+  }
+
+  public static final class HealthEvent.Type.Companion {
+  }
+
+  public final class HeartRateAccuracy extends androidx.health.services.client.data.DataPointAccuracy {
+    ctor public HeartRateAccuracy(androidx.health.services.client.data.HeartRateAccuracy.SensorStatus sensorStatus);
+    method public androidx.health.services.client.data.HeartRateAccuracy.SensorStatus getSensorStatus();
+    property public final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus sensorStatus;
+  }
+
+  public static final class HeartRateAccuracy.SensorStatus {
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus ACCURACY_HIGH;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus ACCURACY_LOW;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus ACCURACY_MEDIUM;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus.Companion Companion;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus NO_CONTACT;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus UNKNOWN;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus UNRELIABLE;
+  }
+
+  public static final class HeartRateAccuracy.SensorStatus.Companion {
+  }
+
+  public final class IntervalDataPoint<T> extends androidx.health.services.client.data.DataPoint<T> {
+    ctor public IntervalDataPoint(androidx.health.services.client.data.DataType<T,? extends androidx.health.services.client.data.IntervalDataPoint<T>> dataType, T value, java.time.Duration startDurationFromBoot, java.time.Duration endDurationFromBoot, optional android.os.Bundle metadata, optional androidx.health.services.client.data.DataPointAccuracy? accuracy);
+    method public androidx.health.services.client.data.DataPointAccuracy? getAccuracy();
+    method public androidx.health.services.client.data.DataType<T,? extends androidx.health.services.client.data.IntervalDataPoint<T>> getDataType();
+    method public java.time.Duration getEndDurationFromBoot();
+    method public java.time.Instant getEndInstant(java.time.Instant bootInstant);
+    method public android.os.Bundle getMetadata();
+    method public java.time.Duration getStartDurationFromBoot();
+    method public java.time.Instant getStartInstant(java.time.Instant bootInstant);
+    method public T getValue();
+    property public final androidx.health.services.client.data.DataPointAccuracy? accuracy;
+    property public androidx.health.services.client.data.DataType<T,? extends androidx.health.services.client.data.IntervalDataPoint<T>> dataType;
+    property public final java.time.Duration endDurationFromBoot;
+    property public final android.os.Bundle metadata;
+    property public final java.time.Duration startDurationFromBoot;
+    property public final T value;
+  }
+
+  public final class LocationAccuracy extends androidx.health.services.client.data.DataPointAccuracy {
+    ctor public LocationAccuracy(@FloatRange(from=0.0) double horizontalPositionErrorMeters, optional @FloatRange(from=0.0) double verticalPositionErrorMeters);
+    method public double getHorizontalPositionErrorMeters();
+    method public double getVerticalPositionErrorMeters();
+    property public final double horizontalPositionErrorMeters;
+    property public final double verticalPositionErrorMeters;
+    field public static final androidx.health.services.client.data.LocationAccuracy.Companion Companion;
+  }
+
+  public static final class LocationAccuracy.Companion {
+  }
+
+  public final class LocationAvailability implements androidx.health.services.client.data.Availability {
+    method public static androidx.health.services.client.data.LocationAvailability? fromId(int id);
+    method public int getId();
+    method public String getName();
+    property public int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.LocationAvailability ACQUIRED_TETHERED;
+    field public static final androidx.health.services.client.data.LocationAvailability ACQUIRED_UNTETHERED;
+    field public static final androidx.health.services.client.data.LocationAvailability ACQUIRING;
+    field public static final androidx.health.services.client.data.LocationAvailability.Companion Companion;
+    field public static final androidx.health.services.client.data.LocationAvailability NO_GNSS;
+    field public static final androidx.health.services.client.data.LocationAvailability UNAVAILABLE;
+    field public static final androidx.health.services.client.data.LocationAvailability UNKNOWN;
+  }
+
+  public static final class LocationAvailability.Companion {
+    method public androidx.health.services.client.data.LocationAvailability? fromId(int id);
+  }
+
+  public final class LocationData {
+    ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional double altitude, optional double bearing);
+    method public double getAltitude();
+    method public double getBearing();
+    method public double getLatitude();
+    method public double getLongitude();
+    property public final double altitude;
+    property public final double bearing;
+    property public final double latitude;
+    property public final double longitude;
+    field public static final double ALTITUDE_UNAVAILABLE = (0.0/0.0);
+    field public static final double BEARING_UNAVAILABLE = (0.0/0.0);
+  }
+
+  public final class MeasureCapabilities {
+    ctor public MeasureCapabilities(java.util.Set<? extends androidx.health.services.client.data.DeltaDataType<?,?>> supportedDataTypesMeasure);
+    method public java.util.Set<androidx.health.services.client.data.DeltaDataType<?,?>> getSupportedDataTypesMeasure();
+    property public final java.util.Set<androidx.health.services.client.data.DeltaDataType<?,?>> supportedDataTypesMeasure;
+  }
+
+  public final class MilestoneMarkerSummary {
+    ctor public MilestoneMarkerSummary(java.time.Instant startTime, java.time.Instant endTime, java.time.Duration activeDuration, androidx.health.services.client.data.ExerciseGoal<? extends java.lang.Number> achievedGoal, androidx.health.services.client.data.DataPointContainer summaryMetrics);
+    method public androidx.health.services.client.data.ExerciseGoal<? extends java.lang.Number> getAchievedGoal();
+    method public java.time.Duration getActiveDuration();
+    method public java.time.Instant getEndTime();
+    method public java.time.Instant getStartTime();
+    method public androidx.health.services.client.data.DataPointContainer getSummaryMetrics();
+    property public final androidx.health.services.client.data.ExerciseGoal<? extends java.lang.Number> achievedGoal;
+    property public final java.time.Duration activeDuration;
+    property public final java.time.Instant endTime;
+    property public final java.time.Instant startTime;
+    property public final androidx.health.services.client.data.DataPointContainer summaryMetrics;
+  }
+
+  public final class PassiveGoal {
+    ctor public PassiveGoal(androidx.health.services.client.data.DataTypeCondition<? extends java.lang.Number,? extends androidx.health.services.client.data.DeltaDataType<? extends java.lang.Number,?>> dataTypeCondition);
+    method public androidx.health.services.client.data.DataTypeCondition<? extends java.lang.Number,? extends androidx.health.services.client.data.DeltaDataType<? extends java.lang.Number,?>> getDataTypeCondition();
+    property public final androidx.health.services.client.data.DataTypeCondition<? extends java.lang.Number,? extends androidx.health.services.client.data.DeltaDataType<? extends java.lang.Number,?>> dataTypeCondition;
+  }
+
+  public final class PassiveListenerConfig {
+    ctor public PassiveListenerConfig(java.util.Set<? extends androidx.health.services.client.data.DataType<? extends java.lang.Object,? extends androidx.health.services.client.data.DataPoint<?>>> dataTypes, boolean shouldUserActivityInfoBeRequested, java.util.Set<androidx.health.services.client.data.PassiveGoal> dailyGoals, java.util.Set<androidx.health.services.client.data.HealthEvent.Type> healthEventTypes);
+    method public static androidx.health.services.client.data.PassiveListenerConfig.Builder builder();
+    method public java.util.Set<androidx.health.services.client.data.PassiveGoal> getDailyGoals();
+    method public java.util.Set<androidx.health.services.client.data.DataType<? extends java.lang.Object,? extends androidx.health.services.client.data.DataPoint<?>>> getDataTypes();
+    method public java.util.Set<androidx.health.services.client.data.HealthEvent.Type> getHealthEventTypes();
+    method public boolean getShouldUserActivityInfoBeRequested();
+    property public final java.util.Set<androidx.health.services.client.data.PassiveGoal> dailyGoals;
+    property public final java.util.Set<androidx.health.services.client.data.DataType<? extends java.lang.Object,? extends androidx.health.services.client.data.DataPoint<?>>> dataTypes;
+    property public final java.util.Set<androidx.health.services.client.data.HealthEvent.Type> healthEventTypes;
+    property public final boolean shouldUserActivityInfoBeRequested;
+    field public static final androidx.health.services.client.data.PassiveListenerConfig.Companion Companion;
+  }
+
+  public static final class PassiveListenerConfig.Builder {
+    ctor public PassiveListenerConfig.Builder();
+    method public androidx.health.services.client.data.PassiveListenerConfig build();
+    method public androidx.health.services.client.data.PassiveListenerConfig.Builder setDailyGoals(java.util.Set<androidx.health.services.client.data.PassiveGoal> dailyGoals);
+    method public androidx.health.services.client.data.PassiveListenerConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
+    method public androidx.health.services.client.data.PassiveListenerConfig.Builder setHealthEventTypes(java.util.Set<androidx.health.services.client.data.HealthEvent.Type> healthEventTypes);
+    method public androidx.health.services.client.data.PassiveListenerConfig.Builder setShouldUserActivityInfoBeRequested(boolean shouldUserActivityInfoBeRequested);
+  }
+
+  public static final class PassiveListenerConfig.Companion {
+    method public androidx.health.services.client.data.PassiveListenerConfig.Builder builder();
+  }
+
+  public final class PassiveMonitoringCapabilities {
+    ctor public PassiveMonitoringCapabilities(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> supportedDataTypesPassiveMonitoring, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> supportedDataTypesPassiveGoals, java.util.Set<androidx.health.services.client.data.HealthEvent.Type> supportedHealthEventTypes, java.util.Set<androidx.health.services.client.data.UserActivityState> supportedUserActivityStates);
+    method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getSupportedDataTypesPassiveGoals();
+    method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getSupportedDataTypesPassiveMonitoring();
+    method public java.util.Set<androidx.health.services.client.data.HealthEvent.Type> getSupportedHealthEventTypes();
+    method public java.util.Set<androidx.health.services.client.data.UserActivityState> getSupportedUserActivityStates();
+    property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> supportedDataTypesPassiveGoals;
+    property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> supportedDataTypesPassiveMonitoring;
+    property public final java.util.Set<androidx.health.services.client.data.HealthEvent.Type> supportedHealthEventTypes;
+    property public final java.util.Set<androidx.health.services.client.data.UserActivityState> supportedUserActivityStates;
+  }
+
+  public final class PassiveMonitoringUpdate {
+    ctor public PassiveMonitoringUpdate(androidx.health.services.client.data.DataPointContainer dataPoints, java.util.List<androidx.health.services.client.data.UserActivityInfo> userActivityInfoUpdates);
+    method public androidx.health.services.client.data.DataPointContainer getDataPoints();
+    method public java.util.List<androidx.health.services.client.data.UserActivityInfo> getUserActivityInfoUpdates();
+    property public final androidx.health.services.client.data.DataPointContainer dataPoints;
+    property public final java.util.List<androidx.health.services.client.data.UserActivityInfo> userActivityInfoUpdates;
+  }
+
+  public final class SampleDataPoint<T> extends androidx.health.services.client.data.DataPoint<T> {
+    ctor public SampleDataPoint(androidx.health.services.client.data.DataType<T,androidx.health.services.client.data.SampleDataPoint<T>> dataType, T value, java.time.Duration timeDurationFromBoot, optional android.os.Bundle metadata, optional androidx.health.services.client.data.DataPointAccuracy? accuracy);
+    method public androidx.health.services.client.data.DataPointAccuracy? getAccuracy();
+    method public androidx.health.services.client.data.DataType<T,androidx.health.services.client.data.SampleDataPoint<T>> getDataType();
+    method public android.os.Bundle getMetadata();
+    method public java.time.Duration getTimeDurationFromBoot();
+    method public java.time.Instant getTimeInstant(java.time.Instant bootInstant);
+    method public T getValue();
+    property public final androidx.health.services.client.data.DataPointAccuracy? accuracy;
+    property public androidx.health.services.client.data.DataType<T,androidx.health.services.client.data.SampleDataPoint<T>> dataType;
+    property public final android.os.Bundle metadata;
+    property public final java.time.Duration timeDurationFromBoot;
+    property public final T value;
+  }
+
+  public final class StatisticalDataPoint<T extends java.lang.Number> extends androidx.health.services.client.data.DataPoint<T> {
+    ctor public StatisticalDataPoint(androidx.health.services.client.data.AggregateDataType<T,androidx.health.services.client.data.StatisticalDataPoint<T>> dataType, T min, T max, T average, java.time.Instant start, java.time.Instant end);
+    method public T getAverage();
+    method public java.time.Instant getEnd();
+    method public T getMax();
+    method public T getMin();
+    method public java.time.Instant getStart();
+    property public final T average;
+    property public final java.time.Instant end;
+    property public final T max;
+    property public final T min;
+    property public final java.time.Instant start;
+    field public static final androidx.health.services.client.data.StatisticalDataPoint.Companion Companion;
+  }
+
+  public static final class StatisticalDataPoint.Companion {
+  }
+
+  public final class UserActivityInfo {
+    ctor public UserActivityInfo(androidx.health.services.client.data.UserActivityState userActivityState, androidx.health.services.client.data.ExerciseInfo? exerciseInfo, java.time.Instant stateChangeTime);
+    method public static androidx.health.services.client.data.UserActivityInfo createActiveExerciseState(androidx.health.services.client.data.ExerciseInfo exerciseInfo, java.time.Instant stateChangeTime);
+    method public static androidx.health.services.client.data.UserActivityInfo createAsleepState(java.time.Instant stateChangeTime);
+    method public static androidx.health.services.client.data.UserActivityInfo createPassiveActivityState(java.time.Instant stateChangeTime);
+    method public static androidx.health.services.client.data.UserActivityInfo createUnknownTypeState(java.time.Instant stateChangeTime);
+    method public androidx.health.services.client.data.ExerciseInfo? getExerciseInfo();
+    method public java.time.Instant getStateChangeTime();
+    method public androidx.health.services.client.data.UserActivityState getUserActivityState();
+    property public final androidx.health.services.client.data.ExerciseInfo? exerciseInfo;
+    property public final java.time.Instant stateChangeTime;
+    property public final androidx.health.services.client.data.UserActivityState userActivityState;
+    field public static final androidx.health.services.client.data.UserActivityInfo.Companion Companion;
+  }
+
+  public static final class UserActivityInfo.Companion {
+    method public androidx.health.services.client.data.UserActivityInfo createActiveExerciseState(androidx.health.services.client.data.ExerciseInfo exerciseInfo, java.time.Instant stateChangeTime);
+    method public androidx.health.services.client.data.UserActivityInfo createAsleepState(java.time.Instant stateChangeTime);
+    method public androidx.health.services.client.data.UserActivityInfo createPassiveActivityState(java.time.Instant stateChangeTime);
+    method public androidx.health.services.client.data.UserActivityInfo createUnknownTypeState(java.time.Instant stateChangeTime);
+  }
+
+  public final class UserActivityState {
+    ctor public UserActivityState(int id, String name);
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.UserActivityState.Companion Companion;
+    field public static final androidx.health.services.client.data.UserActivityState USER_ACTIVITY_ASLEEP;
+    field public static final androidx.health.services.client.data.UserActivityState USER_ACTIVITY_EXERCISE;
+    field public static final androidx.health.services.client.data.UserActivityState USER_ACTIVITY_PASSIVE;
+    field public static final androidx.health.services.client.data.UserActivityState USER_ACTIVITY_UNKNOWN;
+  }
+
+  public static final class UserActivityState.Companion {
+  }
+
+  public final class WarmUpConfig {
+    ctor public WarmUpConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DeltaDataType<?,?>> dataTypes);
+    method public java.util.Set<androidx.health.services.client.data.DeltaDataType<?,?>> getDataTypes();
+    method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    property public final java.util.Set<androidx.health.services.client.data.DeltaDataType<?,?>> dataTypes;
+    property public final androidx.health.services.client.data.ExerciseType exerciseType;
+  }
+
+}
+
diff --git a/health/health-services-client/api/public_plus_experimental_current.txt b/health/health-services-client/api/public_plus_experimental_current.txt
index b985692..9d0902d 100644
--- a/health/health-services-client/api/public_plus_experimental_current.txt
+++ b/health/health-services-client/api/public_plus_experimental_current.txt
@@ -10,6 +10,7 @@
     method public com.google.common.util.concurrent.ListenableFuture<androidx.health.services.client.data.ExerciseInfo> getCurrentExerciseInfoAsync();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> markLapAsync();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> overrideAutoPauseAndResumeForActiveExerciseAsync(boolean enabled);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> overrideBatchingModesForActiveExerciseAsync(java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModes);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> pauseExerciseAsync();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> prepareExerciseAsync(androidx.health.services.client.data.WarmUpConfig configuration);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> removeGoalFromActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
@@ -141,6 +142,14 @@
   public static final class Availability.Companion {
   }
 
+  public final class BatchingMode {
+    field public static final androidx.health.services.client.data.BatchingMode.Companion Companion;
+    field public static final androidx.health.services.client.data.BatchingMode HEART_RATE_5_SECONDS;
+  }
+
+  public static final class BatchingMode.Companion {
+  }
+
   public final class ComparisonType {
     method public int getId();
     method public String getName();
@@ -307,20 +316,25 @@
   }
 
   public final class ExerciseCapabilities {
+    ctor public ExerciseCapabilities(java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities, optional java.util.Set<androidx.health.services.client.data.BatchingMode> supportedBatchingModeOverrides);
     ctor public ExerciseCapabilities(java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities);
     method public java.util.Set<androidx.health.services.client.data.ExerciseType> getAutoPauseAndResumeEnabledExercises();
     method public androidx.health.services.client.data.ExerciseTypeCapabilities getExerciseTypeCapabilities(androidx.health.services.client.data.ExerciseType exercise);
+    method public java.util.Set<androidx.health.services.client.data.BatchingMode> getSupportedBatchingModeOverrides();
     method public java.util.Set<androidx.health.services.client.data.ExerciseType> getSupportedExerciseTypes();
     method public java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> getTypeToCapabilities();
     property public final java.util.Set<androidx.health.services.client.data.ExerciseType> autoPauseAndResumeEnabledExercises;
+    property public final java.util.Set<androidx.health.services.client.data.BatchingMode> supportedBatchingModeOverrides;
     property public final java.util.Set<androidx.health.services.client.data.ExerciseType> supportedExerciseTypes;
     property public final java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities;
   }
 
   public final class ExerciseConfig {
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig, optional java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides);
     ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
     ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
     method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
+    method public java.util.Set<androidx.health.services.client.data.BatchingMode> getBatchingModeOverrides();
     method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
     method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
     method public android.os.Bundle getExerciseParams();
@@ -329,6 +343,7 @@
     method public float getSwimmingPoolLengthMeters();
     method public boolean isAutoPauseAndResumeEnabled();
     method public boolean isGpsEnabled();
+    property public final java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides;
     property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> dataTypes;
     property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
     property public final android.os.Bundle exerciseParams;
@@ -344,6 +359,7 @@
   public static final class ExerciseConfig.Builder {
     ctor public ExerciseConfig.Builder(androidx.health.services.client.data.ExerciseType exerciseType);
     method public androidx.health.services.client.data.ExerciseConfig build();
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setBatchingModeOverrides(java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
diff --git a/health/health-services-client/api/res-1.0.0-beta03.txt b/health/health-services-client/api/res-1.0.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/health/health-services-client/api/res-1.0.0-beta03.txt
diff --git a/health/health-services-client/api/restricted_1.0.0-beta03.txt b/health/health-services-client/api/restricted_1.0.0-beta03.txt
new file mode 100644
index 0000000..9d0902d
--- /dev/null
+++ b/health/health-services-client/api/restricted_1.0.0-beta03.txt
@@ -0,0 +1,909 @@
+// Signature format: 4.0
+package androidx.health.services.client {
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface ExerciseClient {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> addGoalToActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback callback);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> endExerciseAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> flushAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.health.services.client.data.ExerciseCapabilities> getCapabilitiesAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.health.services.client.data.ExerciseInfo> getCurrentExerciseInfoAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> markLapAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> overrideAutoPauseAndResumeForActiveExerciseAsync(boolean enabled);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> overrideBatchingModesForActiveExerciseAsync(java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModes);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> pauseExerciseAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> prepareExerciseAsync(androidx.health.services.client.data.WarmUpConfig configuration);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> removeGoalFromActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> resumeExerciseAsync();
+    method public void setUpdateCallback(androidx.health.services.client.ExerciseUpdateCallback callback);
+    method public void setUpdateCallback(java.util.concurrent.Executor executor, androidx.health.services.client.ExerciseUpdateCallback callback);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig);
+  }
+
+  public final class ExerciseClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? addGoalToActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? clearUpdateCallback(androidx.health.services.client.ExerciseClient, androidx.health.services.client.ExerciseUpdateCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? endExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? flush(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? getCapabilities(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseCapabilities>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? getCurrentExerciseInfo(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.ExerciseInfo>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? markLap(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? overrideAutoPauseAndResumeForActiveExercise(androidx.health.services.client.ExerciseClient, boolean enabled, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? pauseExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? prepareExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.WarmUpConfig configuration, kotlin.coroutines.Continuation<? super kotlin.Unit>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super kotlin.Unit>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? updateExerciseTypeConfig(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+  }
+
+  public interface ExerciseUpdateCallback {
+    method public void onAvailabilityChanged(androidx.health.services.client.data.DataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
+    method public void onExerciseUpdateReceived(androidx.health.services.client.data.ExerciseUpdate update);
+    method public void onLapSummaryReceived(androidx.health.services.client.data.ExerciseLapSummary lapSummary);
+    method public void onRegistered();
+    method public void onRegistrationFailed(Throwable throwable);
+  }
+
+  public final class HealthServices {
+    method public static androidx.health.services.client.HealthServicesClient getClient(android.content.Context context);
+    field public static final androidx.health.services.client.HealthServices INSTANCE;
+  }
+
+  public interface HealthServicesClient {
+    method public androidx.health.services.client.ExerciseClient getExerciseClient();
+    method public androidx.health.services.client.MeasureClient getMeasureClient();
+    method public androidx.health.services.client.PassiveMonitoringClient getPassiveMonitoringClient();
+    property public abstract androidx.health.services.client.ExerciseClient exerciseClient;
+    property public abstract androidx.health.services.client.MeasureClient measureClient;
+    property public abstract androidx.health.services.client.PassiveMonitoringClient passiveMonitoringClient;
+  }
+
+  public final class HealthServicesException extends java.lang.Exception {
+    ctor public HealthServicesException(String message);
+  }
+
+  public final class ListenableFutureExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend <T> Object? awaitWithException(com.google.common.util.concurrent.ListenableFuture<T>, kotlin.coroutines.Continuation<? super T>) throws androidx.health.services.client.HealthServicesException;
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureCallback {
+    method public void onAvailabilityChanged(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.data.Availability availability);
+    method public void onDataReceived(androidx.health.services.client.data.DataPointContainer data);
+    method public default void onRegistered();
+    method public default void onRegistrationFailed(Throwable throwable);
+  }
+
+  public interface MeasureClient {
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.health.services.client.data.MeasureCapabilities> getCapabilitiesAsync();
+    method public void registerMeasureCallback(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
+    method public void registerMeasureCallback(androidx.health.services.client.data.DeltaDataType<?,?> dataType, java.util.concurrent.Executor executor, androidx.health.services.client.MeasureCallback callback);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> unregisterMeasureCallbackAsync(androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback);
+  }
+
+  public final class MeasureClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? getCapabilities(androidx.health.services.client.MeasureClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.MeasureCapabilities>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? unregisterMeasureCallback(androidx.health.services.client.MeasureClient, androidx.health.services.client.data.DeltaDataType<?,?> dataType, androidx.health.services.client.MeasureCallback callback, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface PassiveListenerCallback {
+    method public default void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
+    method public default void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
+    method public default void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
+    method public default void onPermissionLost();
+    method public default void onRegistered();
+    method public default void onRegistrationFailed(Throwable throwable);
+    method public default void onUserActivityInfoReceived(androidx.health.services.client.data.UserActivityInfo info);
+  }
+
+  public abstract class PassiveListenerService extends android.app.Service {
+    ctor public PassiveListenerService();
+    method public final android.os.IBinder? onBind(android.content.Intent intent);
+    method public void onGoalCompleted(androidx.health.services.client.data.PassiveGoal goal);
+    method public void onHealthEventReceived(androidx.health.services.client.data.HealthEvent event);
+    method public void onNewDataPointsReceived(androidx.health.services.client.data.DataPointContainer dataPoints);
+    method public void onPermissionLost();
+    method public void onUserActivityInfoReceived(androidx.health.services.client.data.UserActivityInfo info);
+  }
+
+  public interface PassiveMonitoringClient {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearPassiveListenerCallbackAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> clearPassiveListenerServiceAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> flushAsync();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.health.services.client.data.PassiveMonitoringCapabilities> getCapabilitiesAsync();
+    method public void setPassiveListenerCallback(androidx.health.services.client.data.PassiveListenerConfig config, androidx.health.services.client.PassiveListenerCallback callback);
+    method public void setPassiveListenerCallback(androidx.health.services.client.data.PassiveListenerConfig config, java.util.concurrent.Executor executor, androidx.health.services.client.PassiveListenerCallback callback);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
+  }
+
+  public final class PassiveMonitoringClientExtensionKt {
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? clearPassiveListenerCallback(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? clearPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? flush(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? getCapabilities(androidx.health.services.client.PassiveMonitoringClient, kotlin.coroutines.Continuation<? super androidx.health.services.client.data.PassiveMonitoringCapabilities>) throws androidx.health.services.client.HealthServicesException;
+    method @kotlin.jvm.Throws(exceptionClasses=HealthServicesException::class) public static suspend Object? setPassiveListenerService(androidx.health.services.client.PassiveMonitoringClient, Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config, kotlin.coroutines.Continuation<? super java.lang.Void>) throws androidx.health.services.client.HealthServicesException;
+  }
+
+}
+
+package androidx.health.services.client.data {
+
+  public final class AggregateDataType<T extends java.lang.Number, D extends androidx.health.services.client.data.DataPoint<T>> extends androidx.health.services.client.data.DataType<T,D> {
+    ctor public AggregateDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
+  }
+
+  @kotlin.jvm.JvmDefaultWithCompatibility public interface Availability {
+    method public int getId();
+    property public abstract int id;
+    field public static final androidx.health.services.client.data.Availability.Companion Companion;
+  }
+
+  public static final class Availability.Companion {
+  }
+
+  public final class BatchingMode {
+    field public static final androidx.health.services.client.data.BatchingMode.Companion Companion;
+    field public static final androidx.health.services.client.data.BatchingMode HEART_RATE_5_SECONDS;
+  }
+
+  public static final class BatchingMode.Companion {
+  }
+
+  public final class ComparisonType {
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.ComparisonType.Companion Companion;
+    field public static final androidx.health.services.client.data.ComparisonType GREATER_THAN;
+    field public static final androidx.health.services.client.data.ComparisonType GREATER_THAN_OR_EQUAL;
+    field public static final androidx.health.services.client.data.ComparisonType LESS_THAN;
+    field public static final androidx.health.services.client.data.ComparisonType LESS_THAN_OR_EQUAL;
+    field public static final androidx.health.services.client.data.ComparisonType UNKNOWN;
+  }
+
+  public static final class ComparisonType.Companion {
+  }
+
+  public final class CumulativeDataPoint<T extends java.lang.Number> extends androidx.health.services.client.data.DataPoint<T> {
+    ctor public CumulativeDataPoint(androidx.health.services.client.data.AggregateDataType<T,androidx.health.services.client.data.CumulativeDataPoint<T>> dataType, T total, java.time.Instant start, java.time.Instant end);
+    method public java.time.Instant getEnd();
+    method public java.time.Instant getStart();
+    method public T getTotal();
+    property public final java.time.Instant end;
+    property public final java.time.Instant start;
+    property public final T total;
+  }
+
+  public abstract class DataPoint<T> {
+    method public androidx.health.services.client.data.DataType<T,? extends androidx.health.services.client.data.DataPoint<T>> getDataType();
+    property public androidx.health.services.client.data.DataType<T,? extends androidx.health.services.client.data.DataPoint<T>> dataType;
+  }
+
+  public abstract class DataPointAccuracy {
+    ctor public DataPointAccuracy();
+  }
+
+  public final class DataPointContainer {
+    ctor public DataPointContainer(java.util.Map<androidx.health.services.client.data.DataType<?,?>,? extends java.util.List<? extends androidx.health.services.client.data.DataPoint<?>>> dataPoints);
+    ctor public DataPointContainer(java.util.List<? extends androidx.health.services.client.data.DataPoint<?>> dataPointList);
+    method public java.util.List<androidx.health.services.client.data.CumulativeDataPoint<?>> getCumulativeDataPoints();
+    method public <T, D extends androidx.health.services.client.data.DataPoint<T>> java.util.List<D> getData(androidx.health.services.client.data.DeltaDataType<T,D> type);
+    method public <T extends java.lang.Number, D extends androidx.health.services.client.data.DataPoint<T>> D? getData(androidx.health.services.client.data.AggregateDataType<T,D> type);
+    method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
+    method public java.util.List<androidx.health.services.client.data.IntervalDataPoint<?>> getIntervalDataPoints();
+    method public java.util.List<androidx.health.services.client.data.SampleDataPoint<?>> getSampleDataPoints();
+    method public java.util.List<androidx.health.services.client.data.StatisticalDataPoint<?>> getStatisticalDataPoints();
+    property public final java.util.List<androidx.health.services.client.data.CumulativeDataPoint<?>> cumulativeDataPoints;
+    property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> dataTypes;
+    property public final java.util.List<androidx.health.services.client.data.IntervalDataPoint<?>> intervalDataPoints;
+    property public final java.util.List<androidx.health.services.client.data.SampleDataPoint<?>> sampleDataPoints;
+    property public final java.util.List<androidx.health.services.client.data.StatisticalDataPoint<?>> statisticalDataPoints;
+  }
+
+  public abstract class DataType<T, D extends androidx.health.services.client.data.DataPoint<T>> {
+    ctor public DataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass, boolean isAggregate);
+    method public final String getName();
+    method public final Class<T> getValueClass();
+    property public final String name;
+    property public final Class<T> valueClass;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> ABSOLUTE_ELEVATION;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> ABSOLUTE_ELEVATION_STATS;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> ACTIVE_EXERCISE_DURATION_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> CALORIES;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> CALORIES_DAILY;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> CALORIES_TOTAL;
+    field public static final androidx.health.services.client.data.DataType.Companion Companion;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> DECLINE_DISTANCE;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> DECLINE_DISTANCE_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> DECLINE_DURATION;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> DECLINE_DURATION_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> DISTANCE;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> DISTANCE_DAILY;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> DISTANCE_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> ELEVATION_GAIN;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> ELEVATION_GAIN_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> ELEVATION_LOSS;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> ELEVATION_LOSS_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> FLAT_GROUND_DISTANCE;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> FLAT_GROUND_DISTANCE_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> FLAT_GROUND_DURATION;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> FLAT_GROUND_DURATION_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> FLOORS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> FLOORS_DAILY;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> FLOORS_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> GOLF_SHOT_COUNT;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> GOLF_SHOT_COUNT_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> HEART_RATE_BPM;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> HEART_RATE_BPM_STATS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.IntervalDataPoint<java.lang.Double>> INCLINE_DISTANCE;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Double>> INCLINE_DISTANCE_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> INCLINE_DURATION;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> INCLINE_DURATION_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<androidx.health.services.client.data.LocationData,androidx.health.services.client.data.SampleDataPoint<androidx.health.services.client.data.LocationData>> LOCATION;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> PACE;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> PACE_STATS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> REP_COUNT;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> REP_COUNT_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> RESTING_EXERCISE_DURATION;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> RESTING_EXERCISE_DURATION_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> RUNNING_STEPS;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> RUNNING_STEPS_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> SPEED;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> SPEED_STATS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> STEPS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> STEPS_DAILY;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.SampleDataPoint<java.lang.Long>> STEPS_PER_MINUTE;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Long>> STEPS_PER_MINUTE_STATS;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> STEPS_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> SWIMMING_LAP_COUNT;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> SWIMMING_STROKES;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> SWIMMING_STROKES_TOTAL;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Double,androidx.health.services.client.data.SampleDataPoint<java.lang.Double>> VO2_MAX;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Double,androidx.health.services.client.data.StatisticalDataPoint<java.lang.Double>> VO2_MAX_STATS;
+    field public static final androidx.health.services.client.data.DeltaDataType<java.lang.Long,androidx.health.services.client.data.IntervalDataPoint<java.lang.Long>> WALKING_STEPS;
+    field public static final androidx.health.services.client.data.AggregateDataType<java.lang.Long,androidx.health.services.client.data.CumulativeDataPoint<java.lang.Long>> WALKING_STEPS_TOTAL;
+  }
+
+  public static final class DataType.Companion {
+  }
+
+  public static final class DataType.TimeType {
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.DataType.TimeType.Companion Companion;
+    field public static final androidx.health.services.client.data.DataType.TimeType INTERVAL;
+    field public static final androidx.health.services.client.data.DataType.TimeType SAMPLE;
+    field public static final androidx.health.services.client.data.DataType.TimeType UNKNOWN;
+  }
+
+  public static final class DataType.TimeType.Companion {
+  }
+
+  public final class DataTypeAvailability implements androidx.health.services.client.data.Availability {
+    method public static androidx.health.services.client.data.DataTypeAvailability? fromId(int id);
+    method public int getId();
+    method public String getName();
+    property public int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.DataTypeAvailability ACQUIRING;
+    field public static final androidx.health.services.client.data.DataTypeAvailability AVAILABLE;
+    field public static final androidx.health.services.client.data.DataTypeAvailability.Companion Companion;
+    field public static final androidx.health.services.client.data.DataTypeAvailability UNAVAILABLE;
+    field public static final androidx.health.services.client.data.DataTypeAvailability UNAVAILABLE_DEVICE_OFF_BODY;
+    field public static final androidx.health.services.client.data.DataTypeAvailability UNKNOWN;
+  }
+
+  public static final class DataTypeAvailability.Companion {
+    method public androidx.health.services.client.data.DataTypeAvailability? fromId(int id);
+  }
+
+  public final class DataTypeCondition<T extends java.lang.Number, D extends androidx.health.services.client.data.DataType<T, ? extends androidx.health.services.client.data.DataPoint<T>>> {
+    ctor public DataTypeCondition(D dataType, T threshold, androidx.health.services.client.data.ComparisonType comparisonType);
+    method public androidx.health.services.client.data.ComparisonType getComparisonType();
+    method public D getDataType();
+    method public T getThreshold();
+    property public final androidx.health.services.client.data.ComparisonType comparisonType;
+    property public final D dataType;
+    property public final T threshold;
+  }
+
+  public final class DeltaDataType<T, D extends androidx.health.services.client.data.DataPoint<T>> extends androidx.health.services.client.data.DataType<T,D> {
+    ctor public DeltaDataType(String name, androidx.health.services.client.data.DataType.TimeType timeType, Class<T> valueClass);
+  }
+
+  public final class ExerciseCapabilities {
+    ctor public ExerciseCapabilities(java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities, optional java.util.Set<androidx.health.services.client.data.BatchingMode> supportedBatchingModeOverrides);
+    ctor public ExerciseCapabilities(java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities);
+    method public java.util.Set<androidx.health.services.client.data.ExerciseType> getAutoPauseAndResumeEnabledExercises();
+    method public androidx.health.services.client.data.ExerciseTypeCapabilities getExerciseTypeCapabilities(androidx.health.services.client.data.ExerciseType exercise);
+    method public java.util.Set<androidx.health.services.client.data.BatchingMode> getSupportedBatchingModeOverrides();
+    method public java.util.Set<androidx.health.services.client.data.ExerciseType> getSupportedExerciseTypes();
+    method public java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> getTypeToCapabilities();
+    property public final java.util.Set<androidx.health.services.client.data.ExerciseType> autoPauseAndResumeEnabledExercises;
+    property public final java.util.Set<androidx.health.services.client.data.BatchingMode> supportedBatchingModeOverrides;
+    property public final java.util.Set<androidx.health.services.client.data.ExerciseType> supportedExerciseTypes;
+    property public final java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities;
+  }
+
+  public final class ExerciseConfig {
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig, optional java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
+    method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
+    method public java.util.Set<androidx.health.services.client.data.BatchingMode> getBatchingModeOverrides();
+    method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
+    method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
+    method public android.os.Bundle getExerciseParams();
+    method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    method public androidx.health.services.client.data.ExerciseTypeConfig? getExerciseTypeConfig();
+    method public float getSwimmingPoolLengthMeters();
+    method public boolean isAutoPauseAndResumeEnabled();
+    method public boolean isGpsEnabled();
+    property public final java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides;
+    property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> dataTypes;
+    property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
+    property public final android.os.Bundle exerciseParams;
+    property public final androidx.health.services.client.data.ExerciseType exerciseType;
+    property public final androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig;
+    property public final boolean isAutoPauseAndResumeEnabled;
+    property public final boolean isGpsEnabled;
+    property public final float swimmingPoolLengthMeters;
+    field public static final androidx.health.services.client.data.ExerciseConfig.Companion Companion;
+    field public static final float SWIMMING_POOL_LENGTH_UNSPECIFIED = 0.0f;
+  }
+
+  public static final class ExerciseConfig.Builder {
+    ctor public ExerciseConfig.Builder(androidx.health.services.client.data.ExerciseType exerciseType);
+    method public androidx.health.services.client.data.ExerciseConfig build();
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setBatchingModeOverrides(java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseTypeConfig(androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLengthMeters(float swimmingPoolLength);
+  }
+
+  public static final class ExerciseConfig.Companion {
+    method public androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
+  }
+
+  public final class ExerciseGoal<T extends java.lang.Number> implements android.os.Parcelable {
+    method public static <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createMilestone(androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> condition, T period);
+    method public static <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createMilestoneGoalWithUpdatedThreshold(androidx.health.services.client.data.ExerciseGoal<T> goal, T newThreshold);
+    method public static <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createOneTimeGoal(androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> condition);
+    method public int describeContents();
+    method public androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> getDataTypeCondition();
+    method public androidx.health.services.client.data.ExerciseGoalType getExerciseGoalType();
+    method public T? getPeriod();
+    method public void writeToParcel(android.os.Parcel dest, int flags);
+    property public final androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> dataTypeCondition;
+    property public final androidx.health.services.client.data.ExerciseGoalType exerciseGoalType;
+    property public final T? period;
+    field public static final android.os.Parcelable.Creator<androidx.health.services.client.data.ExerciseGoal<?>> CREATOR;
+    field public static final androidx.health.services.client.data.ExerciseGoal.Companion Companion;
+  }
+
+  public static final class ExerciseGoal.Companion {
+    method public <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createMilestone(androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> condition, T period);
+    method public <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createMilestoneGoalWithUpdatedThreshold(androidx.health.services.client.data.ExerciseGoal<T> goal, T newThreshold);
+    method public <T extends java.lang.Number> androidx.health.services.client.data.ExerciseGoal<T> createOneTimeGoal(androidx.health.services.client.data.DataTypeCondition<T,androidx.health.services.client.data.AggregateDataType<T,?>> condition);
+  }
+
+  public final class ExerciseGoalType {
+    method public static androidx.health.services.client.data.ExerciseGoalType? fromId(int id);
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.ExerciseGoalType.Companion Companion;
+    field public static final androidx.health.services.client.data.ExerciseGoalType MILESTONE;
+    field public static final androidx.health.services.client.data.ExerciseGoalType ONE_TIME_GOAL;
+  }
+
+  public static final class ExerciseGoalType.Companion {
+    method public androidx.health.services.client.data.ExerciseGoalType? fromId(int id);
+  }
+
+  public final class ExerciseInfo {
+    ctor public ExerciseInfo(int exerciseTrackedStatus, androidx.health.services.client.data.ExerciseType exerciseType);
+    method public int getExerciseTrackedStatus();
+    method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    property public final int exerciseTrackedStatus;
+    property public final androidx.health.services.client.data.ExerciseType exerciseType;
+  }
+
+  public final class ExerciseLapSummary {
+    ctor public ExerciseLapSummary(int lapCount, java.time.Instant startTime, java.time.Instant endTime, java.time.Duration activeDuration, androidx.health.services.client.data.DataPointContainer lapMetrics);
+    method public java.time.Duration getActiveDuration();
+    method public java.time.Instant getEndTime();
+    method public int getLapCount();
+    method public androidx.health.services.client.data.DataPointContainer getLapMetrics();
+    method public java.time.Instant getStartTime();
+    property public final java.time.Duration activeDuration;
+    property public final java.time.Instant endTime;
+    property public final int lapCount;
+    property public final androidx.health.services.client.data.DataPointContainer lapMetrics;
+    property public final java.time.Instant startTime;
+  }
+
+  public final class ExerciseState {
+    method public static androidx.health.services.client.data.ExerciseState? fromId(int id);
+    method public int getId();
+    method public String getName();
+    method public boolean isEnded();
+    method public boolean isEnding();
+    method public boolean isPaused();
+    method public boolean isResuming();
+    property public final int id;
+    property public final boolean isEnded;
+    property public final boolean isEnding;
+    property public final boolean isPaused;
+    property public final boolean isResuming;
+    property public final String name;
+    field public static final androidx.health.services.client.data.ExerciseState ACTIVE;
+    field public static final androidx.health.services.client.data.ExerciseState AUTO_PAUSED;
+    field public static final androidx.health.services.client.data.ExerciseState AUTO_PAUSING;
+    field public static final androidx.health.services.client.data.ExerciseState AUTO_RESUMING;
+    field public static final androidx.health.services.client.data.ExerciseState.Companion Companion;
+    field public static final androidx.health.services.client.data.ExerciseState ENDED;
+    field public static final androidx.health.services.client.data.ExerciseState ENDING;
+    field public static final androidx.health.services.client.data.ExerciseState PREPARING;
+    field public static final androidx.health.services.client.data.ExerciseState USER_PAUSED;
+    field public static final androidx.health.services.client.data.ExerciseState USER_PAUSING;
+    field public static final androidx.health.services.client.data.ExerciseState USER_RESUMING;
+    field public static final androidx.health.services.client.data.ExerciseState USER_STARTING;
+  }
+
+  public static final class ExerciseState.Companion {
+    method public androidx.health.services.client.data.ExerciseState? fromId(int id);
+  }
+
+  public final class ExerciseStateInfo {
+    ctor public ExerciseStateInfo(androidx.health.services.client.data.ExerciseState exerciseState, int exerciseEndReason);
+    method public int getEndReason();
+    method public androidx.health.services.client.data.ExerciseState getState();
+    property public final int endReason;
+    property public final androidx.health.services.client.data.ExerciseState state;
+    field public static final androidx.health.services.client.data.ExerciseStateInfo.Companion Companion;
+  }
+
+  public static final class ExerciseStateInfo.Companion {
+  }
+
+  public final class ExerciseType {
+    method public static androidx.health.services.client.data.ExerciseType fromId(int id);
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.ExerciseType ALPINE_SKIING;
+    field public static final androidx.health.services.client.data.ExerciseType BACKPACKING;
+    field public static final androidx.health.services.client.data.ExerciseType BACK_EXTENSION;
+    field public static final androidx.health.services.client.data.ExerciseType BADMINTON;
+    field public static final androidx.health.services.client.data.ExerciseType BARBELL_SHOULDER_PRESS;
+    field public static final androidx.health.services.client.data.ExerciseType BASEBALL;
+    field public static final androidx.health.services.client.data.ExerciseType BASKETBALL;
+    field public static final androidx.health.services.client.data.ExerciseType BENCH_PRESS;
+    field public static final androidx.health.services.client.data.ExerciseType BIKING;
+    field public static final androidx.health.services.client.data.ExerciseType BIKING_STATIONARY;
+    field public static final androidx.health.services.client.data.ExerciseType BOOT_CAMP;
+    field public static final androidx.health.services.client.data.ExerciseType BOXING;
+    field public static final androidx.health.services.client.data.ExerciseType BURPEE;
+    field public static final androidx.health.services.client.data.ExerciseType CALISTHENICS;
+    field public static final androidx.health.services.client.data.ExerciseType CRICKET;
+    field public static final androidx.health.services.client.data.ExerciseType CROSS_COUNTRY_SKIING;
+    field public static final androidx.health.services.client.data.ExerciseType CRUNCH;
+    field public static final androidx.health.services.client.data.ExerciseType.Companion Companion;
+    field public static final androidx.health.services.client.data.ExerciseType DANCING;
+    field public static final androidx.health.services.client.data.ExerciseType DEADLIFT;
+    field public static final androidx.health.services.client.data.ExerciseType ELLIPTICAL;
+    field public static final androidx.health.services.client.data.ExerciseType EXERCISE_CLASS;
+    field public static final androidx.health.services.client.data.ExerciseType FENCING;
+    field public static final androidx.health.services.client.data.ExerciseType FOOTBALL_AMERICAN;
+    field public static final androidx.health.services.client.data.ExerciseType FOOTBALL_AUSTRALIAN;
+    field public static final androidx.health.services.client.data.ExerciseType FORWARD_TWIST;
+    field public static final androidx.health.services.client.data.ExerciseType FRISBEE_DISC;
+    field public static final androidx.health.services.client.data.ExerciseType GOLF;
+    field public static final androidx.health.services.client.data.ExerciseType GUIDED_BREATHING;
+    field public static final androidx.health.services.client.data.ExerciseType GYMNASTICS;
+    field public static final androidx.health.services.client.data.ExerciseType HANDBALL;
+    field public static final androidx.health.services.client.data.ExerciseType HIGH_INTENSITY_INTERVAL_TRAINING;
+    field public static final androidx.health.services.client.data.ExerciseType HIKING;
+    field public static final androidx.health.services.client.data.ExerciseType HORSE_RIDING;
+    field public static final androidx.health.services.client.data.ExerciseType ICE_HOCKEY;
+    field public static final androidx.health.services.client.data.ExerciseType ICE_SKATING;
+    field public static final androidx.health.services.client.data.ExerciseType INLINE_SKATING;
+    field public static final androidx.health.services.client.data.ExerciseType JUMPING_JACK;
+    field public static final androidx.health.services.client.data.ExerciseType JUMP_ROPE;
+    field public static final androidx.health.services.client.data.ExerciseType LAT_PULL_DOWN;
+    field public static final androidx.health.services.client.data.ExerciseType LUNGE;
+    field public static final androidx.health.services.client.data.ExerciseType MARTIAL_ARTS;
+    field public static final androidx.health.services.client.data.ExerciseType MEDITATION;
+    field public static final androidx.health.services.client.data.ExerciseType MOUNTAIN_BIKING;
+    field public static final androidx.health.services.client.data.ExerciseType ORIENTEERING;
+    field public static final androidx.health.services.client.data.ExerciseType PADDLING;
+    field public static final androidx.health.services.client.data.ExerciseType PARA_GLIDING;
+    field public static final androidx.health.services.client.data.ExerciseType PILATES;
+    field public static final androidx.health.services.client.data.ExerciseType PLANK;
+    field public static final androidx.health.services.client.data.ExerciseType RACQUETBALL;
+    field public static final androidx.health.services.client.data.ExerciseType ROCK_CLIMBING;
+    field public static final androidx.health.services.client.data.ExerciseType ROLLER_HOCKEY;
+    field public static final androidx.health.services.client.data.ExerciseType ROLLER_SKATING;
+    field public static final androidx.health.services.client.data.ExerciseType ROWING;
+    field public static final androidx.health.services.client.data.ExerciseType ROWING_MACHINE;
+    field public static final androidx.health.services.client.data.ExerciseType RUGBY;
+    field public static final androidx.health.services.client.data.ExerciseType RUNNING;
+    field public static final androidx.health.services.client.data.ExerciseType RUNNING_TREADMILL;
+    field public static final androidx.health.services.client.data.ExerciseType SAILING;
+    field public static final androidx.health.services.client.data.ExerciseType SCUBA_DIVING;
+    field public static final androidx.health.services.client.data.ExerciseType SKATING;
+    field public static final androidx.health.services.client.data.ExerciseType SKIING;
+    field public static final androidx.health.services.client.data.ExerciseType SNOWBOARDING;
+    field public static final androidx.health.services.client.data.ExerciseType SNOWSHOEING;
+    field public static final androidx.health.services.client.data.ExerciseType SOCCER;
+    field public static final androidx.health.services.client.data.ExerciseType SOFTBALL;
+    field public static final androidx.health.services.client.data.ExerciseType SQUASH;
+    field public static final androidx.health.services.client.data.ExerciseType SQUAT;
+    field public static final androidx.health.services.client.data.ExerciseType STAIR_CLIMBING;
+    field public static final androidx.health.services.client.data.ExerciseType STAIR_CLIMBING_MACHINE;
+    field public static final androidx.health.services.client.data.ExerciseType STRENGTH_TRAINING;
+    field public static final androidx.health.services.client.data.ExerciseType STRETCHING;
+    field public static final androidx.health.services.client.data.ExerciseType SURFING;
+    field public static final androidx.health.services.client.data.ExerciseType SWIMMING_OPEN_WATER;
+    field public static final androidx.health.services.client.data.ExerciseType SWIMMING_POOL;
+    field public static final androidx.health.services.client.data.ExerciseType TABLE_TENNIS;
+    field public static final androidx.health.services.client.data.ExerciseType TENNIS;
+    field public static final androidx.health.services.client.data.ExerciseType UNKNOWN;
+    field public static final androidx.health.services.client.data.ExerciseType UPPER_TWIST;
+    field public static final androidx.health.services.client.data.ExerciseType VOLLEYBALL;
+    field public static final androidx.health.services.client.data.ExerciseType WALKING;
+    field public static final androidx.health.services.client.data.ExerciseType WATER_POLO;
+    field public static final androidx.health.services.client.data.ExerciseType WEIGHTLIFTING;
+    field public static final androidx.health.services.client.data.ExerciseType WORKOUT;
+    field public static final androidx.health.services.client.data.ExerciseType YACHTING;
+    field public static final androidx.health.services.client.data.ExerciseType YOGA;
+  }
+
+  public static final class ExerciseType.Companion {
+    method public androidx.health.services.client.data.ExerciseType fromId(int id);
+  }
+
+  public final class ExerciseTypeCapabilities {
+    ctor public ExerciseTypeCapabilities(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> supportedDataTypes, java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,? extends java.util.Set<androidx.health.services.client.data.ComparisonType>> supportedGoals, java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,? extends java.util.Set<androidx.health.services.client.data.ComparisonType>> supportedMilestones, boolean supportsAutoPauseAndResume);
+    method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getSupportedDataTypes();
+    method public java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,java.util.Set<androidx.health.services.client.data.ComparisonType>> getSupportedGoals();
+    method public java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,java.util.Set<androidx.health.services.client.data.ComparisonType>> getSupportedMilestones();
+    method public boolean getSupportsAutoPauseAndResume();
+    property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> supportedDataTypes;
+    property public final java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,java.util.Set<androidx.health.services.client.data.ComparisonType>> supportedGoals;
+    property public final java.util.Map<androidx.health.services.client.data.AggregateDataType<?,?>,java.util.Set<androidx.health.services.client.data.ComparisonType>> supportedMilestones;
+    property public final boolean supportsAutoPauseAndResume;
+  }
+
+  public abstract class ExerciseTypeConfig {
+    field public static final androidx.health.services.client.data.ExerciseTypeConfig.Companion Companion;
+  }
+
+  public static final class ExerciseTypeConfig.Companion {
+  }
+
+  public final class ExerciseUpdate {
+    method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.IntervalDataPoint<?> dataPoint);
+    method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.SampleDataPoint<?> dataPoint);
+    method public androidx.health.services.client.data.ExerciseUpdate.ActiveDurationCheckpoint? getActiveDurationCheckpoint();
+    method public androidx.health.services.client.data.ExerciseConfig? getExerciseConfig();
+    method public androidx.health.services.client.data.ExerciseStateInfo getExerciseStateInfo();
+    method public java.util.Set<androidx.health.services.client.data.ExerciseGoal<? extends java.lang.Number>> getLatestAchievedGoals();
+    method public androidx.health.services.client.data.DataPointContainer getLatestMetrics();
+    method public java.util.Set<androidx.health.services.client.data.MilestoneMarkerSummary> getLatestMilestoneMarkerSummaries();
+    method public java.time.Instant? getStartTime();
+    method public java.time.Duration getUpdateDurationFromBoot();
+    property public final androidx.health.services.client.data.ExerciseUpdate.ActiveDurationCheckpoint? activeDurationCheckpoint;
+    property public final androidx.health.services.client.data.ExerciseConfig? exerciseConfig;
+    property public final androidx.health.services.client.data.ExerciseStateInfo exerciseStateInfo;
+    property public final java.util.Set<androidx.health.services.client.data.ExerciseGoal<? extends java.lang.Number>> latestAchievedGoals;
+    property public final androidx.health.services.client.data.DataPointContainer latestMetrics;
+    property public final java.util.Set<androidx.health.services.client.data.MilestoneMarkerSummary> latestMilestoneMarkerSummaries;
+    property public final java.time.Instant? startTime;
+    field public static final androidx.health.services.client.data.ExerciseUpdate.Companion Companion;
+  }
+
+  public static final class ExerciseUpdate.ActiveDurationCheckpoint {
+    ctor public ExerciseUpdate.ActiveDurationCheckpoint(java.time.Instant time, java.time.Duration activeDuration);
+    method public java.time.Duration getActiveDuration();
+    method public java.time.Instant getTime();
+    property public final java.time.Duration activeDuration;
+    property public final java.time.Instant time;
+  }
+
+  public static final class ExerciseUpdate.Companion {
+  }
+
+  public final class GolfExerciseTypeConfig extends androidx.health.services.client.data.ExerciseTypeConfig {
+    ctor public GolfExerciseTypeConfig(optional androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo golfShotTrackingPlaceInfo);
+    method public androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo getGolfShotTrackingPlaceInfo();
+    property public final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo golfShotTrackingPlaceInfo;
+  }
+
+  public static final class GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo {
+    method public int getPlaceInfoId();
+    property public final int placeInfoId;
+    field public static final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo.Companion Companion;
+    field public static final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo GOLF_SHOT_TRACKING_PLACE_INFO_FAIRWAY;
+    field public static final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo GOLF_SHOT_TRACKING_PLACE_INFO_PUTTING_GREEN;
+    field public static final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo GOLF_SHOT_TRACKING_PLACE_INFO_TEE_BOX;
+    field public static final androidx.health.services.client.data.GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo GOLF_SHOT_TRACKING_PLACE_INFO_UNSPECIFIED;
+  }
+
+  public static final class GolfExerciseTypeConfig.GolfShotTrackingPlaceInfo.Companion {
+  }
+
+  public final class HealthEvent {
+    ctor public HealthEvent(androidx.health.services.client.data.HealthEvent.Type type, java.time.Instant eventTime, androidx.health.services.client.data.DataPointContainer metrics);
+    method public java.time.Instant getEventTime();
+    method public androidx.health.services.client.data.DataPointContainer getMetrics();
+    method public androidx.health.services.client.data.HealthEvent.Type getType();
+    property public final java.time.Instant eventTime;
+    property public final androidx.health.services.client.data.DataPointContainer metrics;
+    property public final androidx.health.services.client.data.HealthEvent.Type type;
+  }
+
+  public static final class HealthEvent.Type {
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.HealthEvent.Type.Companion Companion;
+    field public static final androidx.health.services.client.data.HealthEvent.Type FALL_DETECTED;
+    field public static final androidx.health.services.client.data.HealthEvent.Type UNKNOWN;
+  }
+
+  public static final class HealthEvent.Type.Companion {
+  }
+
+  public final class HeartRateAccuracy extends androidx.health.services.client.data.DataPointAccuracy {
+    ctor public HeartRateAccuracy(androidx.health.services.client.data.HeartRateAccuracy.SensorStatus sensorStatus);
+    method public androidx.health.services.client.data.HeartRateAccuracy.SensorStatus getSensorStatus();
+    property public final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus sensorStatus;
+  }
+
+  public static final class HeartRateAccuracy.SensorStatus {
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus ACCURACY_HIGH;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus ACCURACY_LOW;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus ACCURACY_MEDIUM;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus.Companion Companion;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus NO_CONTACT;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus UNKNOWN;
+    field public static final androidx.health.services.client.data.HeartRateAccuracy.SensorStatus UNRELIABLE;
+  }
+
+  public static final class HeartRateAccuracy.SensorStatus.Companion {
+  }
+
+  public final class IntervalDataPoint<T> extends androidx.health.services.client.data.DataPoint<T> {
+    ctor public IntervalDataPoint(androidx.health.services.client.data.DataType<T,? extends androidx.health.services.client.data.IntervalDataPoint<T>> dataType, T value, java.time.Duration startDurationFromBoot, java.time.Duration endDurationFromBoot, optional android.os.Bundle metadata, optional androidx.health.services.client.data.DataPointAccuracy? accuracy);
+    method public androidx.health.services.client.data.DataPointAccuracy? getAccuracy();
+    method public androidx.health.services.client.data.DataType<T,? extends androidx.health.services.client.data.IntervalDataPoint<T>> getDataType();
+    method public java.time.Duration getEndDurationFromBoot();
+    method public java.time.Instant getEndInstant(java.time.Instant bootInstant);
+    method public android.os.Bundle getMetadata();
+    method public java.time.Duration getStartDurationFromBoot();
+    method public java.time.Instant getStartInstant(java.time.Instant bootInstant);
+    method public T getValue();
+    property public final androidx.health.services.client.data.DataPointAccuracy? accuracy;
+    property public androidx.health.services.client.data.DataType<T,? extends androidx.health.services.client.data.IntervalDataPoint<T>> dataType;
+    property public final java.time.Duration endDurationFromBoot;
+    property public final android.os.Bundle metadata;
+    property public final java.time.Duration startDurationFromBoot;
+    property public final T value;
+  }
+
+  public final class LocationAccuracy extends androidx.health.services.client.data.DataPointAccuracy {
+    ctor public LocationAccuracy(@FloatRange(from=0.0) double horizontalPositionErrorMeters, optional @FloatRange(from=0.0) double verticalPositionErrorMeters);
+    method public double getHorizontalPositionErrorMeters();
+    method public double getVerticalPositionErrorMeters();
+    property public final double horizontalPositionErrorMeters;
+    property public final double verticalPositionErrorMeters;
+    field public static final androidx.health.services.client.data.LocationAccuracy.Companion Companion;
+  }
+
+  public static final class LocationAccuracy.Companion {
+  }
+
+  public final class LocationAvailability implements androidx.health.services.client.data.Availability {
+    method public static androidx.health.services.client.data.LocationAvailability? fromId(int id);
+    method public int getId();
+    method public String getName();
+    property public int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.LocationAvailability ACQUIRED_TETHERED;
+    field public static final androidx.health.services.client.data.LocationAvailability ACQUIRED_UNTETHERED;
+    field public static final androidx.health.services.client.data.LocationAvailability ACQUIRING;
+    field public static final androidx.health.services.client.data.LocationAvailability.Companion Companion;
+    field public static final androidx.health.services.client.data.LocationAvailability NO_GNSS;
+    field public static final androidx.health.services.client.data.LocationAvailability UNAVAILABLE;
+    field public static final androidx.health.services.client.data.LocationAvailability UNKNOWN;
+  }
+
+  public static final class LocationAvailability.Companion {
+    method public androidx.health.services.client.data.LocationAvailability? fromId(int id);
+  }
+
+  public final class LocationData {
+    ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional double altitude, optional double bearing);
+    method public double getAltitude();
+    method public double getBearing();
+    method public double getLatitude();
+    method public double getLongitude();
+    property public final double altitude;
+    property public final double bearing;
+    property public final double latitude;
+    property public final double longitude;
+    field public static final double ALTITUDE_UNAVAILABLE = (0.0/0.0);
+    field public static final double BEARING_UNAVAILABLE = (0.0/0.0);
+  }
+
+  public final class MeasureCapabilities {
+    ctor public MeasureCapabilities(java.util.Set<? extends androidx.health.services.client.data.DeltaDataType<?,?>> supportedDataTypesMeasure);
+    method public java.util.Set<androidx.health.services.client.data.DeltaDataType<?,?>> getSupportedDataTypesMeasure();
+    property public final java.util.Set<androidx.health.services.client.data.DeltaDataType<?,?>> supportedDataTypesMeasure;
+  }
+
+  public final class MilestoneMarkerSummary {
+    ctor public MilestoneMarkerSummary(java.time.Instant startTime, java.time.Instant endTime, java.time.Duration activeDuration, androidx.health.services.client.data.ExerciseGoal<? extends java.lang.Number> achievedGoal, androidx.health.services.client.data.DataPointContainer summaryMetrics);
+    method public androidx.health.services.client.data.ExerciseGoal<? extends java.lang.Number> getAchievedGoal();
+    method public java.time.Duration getActiveDuration();
+    method public java.time.Instant getEndTime();
+    method public java.time.Instant getStartTime();
+    method public androidx.health.services.client.data.DataPointContainer getSummaryMetrics();
+    property public final androidx.health.services.client.data.ExerciseGoal<? extends java.lang.Number> achievedGoal;
+    property public final java.time.Duration activeDuration;
+    property public final java.time.Instant endTime;
+    property public final java.time.Instant startTime;
+    property public final androidx.health.services.client.data.DataPointContainer summaryMetrics;
+  }
+
+  public final class PassiveGoal {
+    ctor public PassiveGoal(androidx.health.services.client.data.DataTypeCondition<? extends java.lang.Number,? extends androidx.health.services.client.data.DeltaDataType<? extends java.lang.Number,?>> dataTypeCondition);
+    method public androidx.health.services.client.data.DataTypeCondition<? extends java.lang.Number,? extends androidx.health.services.client.data.DeltaDataType<? extends java.lang.Number,?>> getDataTypeCondition();
+    property public final androidx.health.services.client.data.DataTypeCondition<? extends java.lang.Number,? extends androidx.health.services.client.data.DeltaDataType<? extends java.lang.Number,?>> dataTypeCondition;
+  }
+
+  public final class PassiveListenerConfig {
+    ctor public PassiveListenerConfig(java.util.Set<? extends androidx.health.services.client.data.DataType<? extends java.lang.Object,? extends androidx.health.services.client.data.DataPoint<?>>> dataTypes, boolean shouldUserActivityInfoBeRequested, java.util.Set<androidx.health.services.client.data.PassiveGoal> dailyGoals, java.util.Set<androidx.health.services.client.data.HealthEvent.Type> healthEventTypes);
+    method public static androidx.health.services.client.data.PassiveListenerConfig.Builder builder();
+    method public java.util.Set<androidx.health.services.client.data.PassiveGoal> getDailyGoals();
+    method public java.util.Set<androidx.health.services.client.data.DataType<? extends java.lang.Object,? extends androidx.health.services.client.data.DataPoint<?>>> getDataTypes();
+    method public java.util.Set<androidx.health.services.client.data.HealthEvent.Type> getHealthEventTypes();
+    method public boolean getShouldUserActivityInfoBeRequested();
+    property public final java.util.Set<androidx.health.services.client.data.PassiveGoal> dailyGoals;
+    property public final java.util.Set<androidx.health.services.client.data.DataType<? extends java.lang.Object,? extends androidx.health.services.client.data.DataPoint<?>>> dataTypes;
+    property public final java.util.Set<androidx.health.services.client.data.HealthEvent.Type> healthEventTypes;
+    property public final boolean shouldUserActivityInfoBeRequested;
+    field public static final androidx.health.services.client.data.PassiveListenerConfig.Companion Companion;
+  }
+
+  public static final class PassiveListenerConfig.Builder {
+    ctor public PassiveListenerConfig.Builder();
+    method public androidx.health.services.client.data.PassiveListenerConfig build();
+    method public androidx.health.services.client.data.PassiveListenerConfig.Builder setDailyGoals(java.util.Set<androidx.health.services.client.data.PassiveGoal> dailyGoals);
+    method public androidx.health.services.client.data.PassiveListenerConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
+    method public androidx.health.services.client.data.PassiveListenerConfig.Builder setHealthEventTypes(java.util.Set<androidx.health.services.client.data.HealthEvent.Type> healthEventTypes);
+    method public androidx.health.services.client.data.PassiveListenerConfig.Builder setShouldUserActivityInfoBeRequested(boolean shouldUserActivityInfoBeRequested);
+  }
+
+  public static final class PassiveListenerConfig.Companion {
+    method public androidx.health.services.client.data.PassiveListenerConfig.Builder builder();
+  }
+
+  public final class PassiveMonitoringCapabilities {
+    ctor public PassiveMonitoringCapabilities(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> supportedDataTypesPassiveMonitoring, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> supportedDataTypesPassiveGoals, java.util.Set<androidx.health.services.client.data.HealthEvent.Type> supportedHealthEventTypes, java.util.Set<androidx.health.services.client.data.UserActivityState> supportedUserActivityStates);
+    method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getSupportedDataTypesPassiveGoals();
+    method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getSupportedDataTypesPassiveMonitoring();
+    method public java.util.Set<androidx.health.services.client.data.HealthEvent.Type> getSupportedHealthEventTypes();
+    method public java.util.Set<androidx.health.services.client.data.UserActivityState> getSupportedUserActivityStates();
+    property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> supportedDataTypesPassiveGoals;
+    property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> supportedDataTypesPassiveMonitoring;
+    property public final java.util.Set<androidx.health.services.client.data.HealthEvent.Type> supportedHealthEventTypes;
+    property public final java.util.Set<androidx.health.services.client.data.UserActivityState> supportedUserActivityStates;
+  }
+
+  public final class PassiveMonitoringUpdate {
+    ctor public PassiveMonitoringUpdate(androidx.health.services.client.data.DataPointContainer dataPoints, java.util.List<androidx.health.services.client.data.UserActivityInfo> userActivityInfoUpdates);
+    method public androidx.health.services.client.data.DataPointContainer getDataPoints();
+    method public java.util.List<androidx.health.services.client.data.UserActivityInfo> getUserActivityInfoUpdates();
+    property public final androidx.health.services.client.data.DataPointContainer dataPoints;
+    property public final java.util.List<androidx.health.services.client.data.UserActivityInfo> userActivityInfoUpdates;
+  }
+
+  public final class SampleDataPoint<T> extends androidx.health.services.client.data.DataPoint<T> {
+    ctor public SampleDataPoint(androidx.health.services.client.data.DataType<T,androidx.health.services.client.data.SampleDataPoint<T>> dataType, T value, java.time.Duration timeDurationFromBoot, optional android.os.Bundle metadata, optional androidx.health.services.client.data.DataPointAccuracy? accuracy);
+    method public androidx.health.services.client.data.DataPointAccuracy? getAccuracy();
+    method public androidx.health.services.client.data.DataType<T,androidx.health.services.client.data.SampleDataPoint<T>> getDataType();
+    method public android.os.Bundle getMetadata();
+    method public java.time.Duration getTimeDurationFromBoot();
+    method public java.time.Instant getTimeInstant(java.time.Instant bootInstant);
+    method public T getValue();
+    property public final androidx.health.services.client.data.DataPointAccuracy? accuracy;
+    property public androidx.health.services.client.data.DataType<T,androidx.health.services.client.data.SampleDataPoint<T>> dataType;
+    property public final android.os.Bundle metadata;
+    property public final java.time.Duration timeDurationFromBoot;
+    property public final T value;
+  }
+
+  public final class StatisticalDataPoint<T extends java.lang.Number> extends androidx.health.services.client.data.DataPoint<T> {
+    ctor public StatisticalDataPoint(androidx.health.services.client.data.AggregateDataType<T,androidx.health.services.client.data.StatisticalDataPoint<T>> dataType, T min, T max, T average, java.time.Instant start, java.time.Instant end);
+    method public T getAverage();
+    method public java.time.Instant getEnd();
+    method public T getMax();
+    method public T getMin();
+    method public java.time.Instant getStart();
+    property public final T average;
+    property public final java.time.Instant end;
+    property public final T max;
+    property public final T min;
+    property public final java.time.Instant start;
+    field public static final androidx.health.services.client.data.StatisticalDataPoint.Companion Companion;
+  }
+
+  public static final class StatisticalDataPoint.Companion {
+  }
+
+  public final class UserActivityInfo {
+    ctor public UserActivityInfo(androidx.health.services.client.data.UserActivityState userActivityState, androidx.health.services.client.data.ExerciseInfo? exerciseInfo, java.time.Instant stateChangeTime);
+    method public static androidx.health.services.client.data.UserActivityInfo createActiveExerciseState(androidx.health.services.client.data.ExerciseInfo exerciseInfo, java.time.Instant stateChangeTime);
+    method public static androidx.health.services.client.data.UserActivityInfo createAsleepState(java.time.Instant stateChangeTime);
+    method public static androidx.health.services.client.data.UserActivityInfo createPassiveActivityState(java.time.Instant stateChangeTime);
+    method public static androidx.health.services.client.data.UserActivityInfo createUnknownTypeState(java.time.Instant stateChangeTime);
+    method public androidx.health.services.client.data.ExerciseInfo? getExerciseInfo();
+    method public java.time.Instant getStateChangeTime();
+    method public androidx.health.services.client.data.UserActivityState getUserActivityState();
+    property public final androidx.health.services.client.data.ExerciseInfo? exerciseInfo;
+    property public final java.time.Instant stateChangeTime;
+    property public final androidx.health.services.client.data.UserActivityState userActivityState;
+    field public static final androidx.health.services.client.data.UserActivityInfo.Companion Companion;
+  }
+
+  public static final class UserActivityInfo.Companion {
+    method public androidx.health.services.client.data.UserActivityInfo createActiveExerciseState(androidx.health.services.client.data.ExerciseInfo exerciseInfo, java.time.Instant stateChangeTime);
+    method public androidx.health.services.client.data.UserActivityInfo createAsleepState(java.time.Instant stateChangeTime);
+    method public androidx.health.services.client.data.UserActivityInfo createPassiveActivityState(java.time.Instant stateChangeTime);
+    method public androidx.health.services.client.data.UserActivityInfo createUnknownTypeState(java.time.Instant stateChangeTime);
+  }
+
+  public final class UserActivityState {
+    ctor public UserActivityState(int id, String name);
+    method public int getId();
+    method public String getName();
+    property public final int id;
+    property public final String name;
+    field public static final androidx.health.services.client.data.UserActivityState.Companion Companion;
+    field public static final androidx.health.services.client.data.UserActivityState USER_ACTIVITY_ASLEEP;
+    field public static final androidx.health.services.client.data.UserActivityState USER_ACTIVITY_EXERCISE;
+    field public static final androidx.health.services.client.data.UserActivityState USER_ACTIVITY_PASSIVE;
+    field public static final androidx.health.services.client.data.UserActivityState USER_ACTIVITY_UNKNOWN;
+  }
+
+  public static final class UserActivityState.Companion {
+  }
+
+  public final class WarmUpConfig {
+    ctor public WarmUpConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DeltaDataType<?,?>> dataTypes);
+    method public java.util.Set<androidx.health.services.client.data.DeltaDataType<?,?>> getDataTypes();
+    method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    property public final java.util.Set<androidx.health.services.client.data.DeltaDataType<?,?>> dataTypes;
+    property public final androidx.health.services.client.data.ExerciseType exerciseType;
+  }
+
+}
+
diff --git a/health/health-services-client/api/restricted_current.ignore b/health/health-services-client/api/restricted_current.ignore
index a882151..091f290 100644
--- a/health/health-services-client/api/restricted_current.ignore
+++ b/health/health-services-client/api/restricted_current.ignore
@@ -1,3 +1,3 @@
 // Baseline format: 1.0
-AddedAbstractMethod: androidx.health.services.client.ExerciseClient#updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig):
-    Added method androidx.health.services.client.ExerciseClient.updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig)
+AddedAbstractMethod: androidx.health.services.client.ExerciseClient#overrideBatchingModesForActiveExerciseAsync(java.util.Set<androidx.health.services.client.data.BatchingMode>):
+    Added method androidx.health.services.client.ExerciseClient.overrideBatchingModesForActiveExerciseAsync(java.util.Set<androidx.health.services.client.data.BatchingMode>)
diff --git a/health/health-services-client/api/restricted_current.txt b/health/health-services-client/api/restricted_current.txt
index b985692..9d0902d 100644
--- a/health/health-services-client/api/restricted_current.txt
+++ b/health/health-services-client/api/restricted_current.txt
@@ -10,6 +10,7 @@
     method public com.google.common.util.concurrent.ListenableFuture<androidx.health.services.client.data.ExerciseInfo> getCurrentExerciseInfoAsync();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> markLapAsync();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> overrideAutoPauseAndResumeForActiveExerciseAsync(boolean enabled);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> overrideBatchingModesForActiveExerciseAsync(java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModes);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> pauseExerciseAsync();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> prepareExerciseAsync(androidx.health.services.client.data.WarmUpConfig configuration);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> removeGoalFromActiveExerciseAsync(androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal);
@@ -141,6 +142,14 @@
   public static final class Availability.Companion {
   }
 
+  public final class BatchingMode {
+    field public static final androidx.health.services.client.data.BatchingMode.Companion Companion;
+    field public static final androidx.health.services.client.data.BatchingMode HEART_RATE_5_SECONDS;
+  }
+
+  public static final class BatchingMode.Companion {
+  }
+
   public final class ComparisonType {
     method public int getId();
     method public String getName();
@@ -307,20 +316,25 @@
   }
 
   public final class ExerciseCapabilities {
+    ctor public ExerciseCapabilities(java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities, optional java.util.Set<androidx.health.services.client.data.BatchingMode> supportedBatchingModeOverrides);
     ctor public ExerciseCapabilities(java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities);
     method public java.util.Set<androidx.health.services.client.data.ExerciseType> getAutoPauseAndResumeEnabledExercises();
     method public androidx.health.services.client.data.ExerciseTypeCapabilities getExerciseTypeCapabilities(androidx.health.services.client.data.ExerciseType exercise);
+    method public java.util.Set<androidx.health.services.client.data.BatchingMode> getSupportedBatchingModeOverrides();
     method public java.util.Set<androidx.health.services.client.data.ExerciseType> getSupportedExerciseTypes();
     method public java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> getTypeToCapabilities();
     property public final java.util.Set<androidx.health.services.client.data.ExerciseType> autoPauseAndResumeEnabledExercises;
+    property public final java.util.Set<androidx.health.services.client.data.BatchingMode> supportedBatchingModeOverrides;
     property public final java.util.Set<androidx.health.services.client.data.ExerciseType> supportedExerciseTypes;
     property public final java.util.Map<androidx.health.services.client.data.ExerciseType,androidx.health.services.client.data.ExerciseTypeCapabilities> typeToCapabilities;
   }
 
   public final class ExerciseConfig {
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig, optional java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides);
     ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
     ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
     method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
+    method public java.util.Set<androidx.health.services.client.data.BatchingMode> getBatchingModeOverrides();
     method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
     method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
     method public android.os.Bundle getExerciseParams();
@@ -329,6 +343,7 @@
     method public float getSwimmingPoolLengthMeters();
     method public boolean isAutoPauseAndResumeEnabled();
     method public boolean isGpsEnabled();
+    property public final java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides;
     property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> dataTypes;
     property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
     property public final android.os.Bundle exerciseParams;
@@ -344,6 +359,7 @@
   public static final class ExerciseConfig.Builder {
     ctor public ExerciseConfig.Builder(androidx.health.services.client.data.ExerciseType exerciseType);
     method public androidx.health.services.client.data.ExerciseConfig build();
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setBatchingModeOverrides(java.util.Set<androidx.health.services.client.data.BatchingMode> batchingModeOverrides);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
diff --git a/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IExerciseApiService.aidl b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IExerciseApiService.aidl
index 87f3b3e..b52a78c 100644
--- a/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IExerciseApiService.aidl
+++ b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IExerciseApiService.aidl
@@ -20,6 +20,7 @@
 import androidx.health.services.client.impl.internal.IExerciseInfoCallback;
 import androidx.health.services.client.impl.internal.IStatusCallback;
 import androidx.health.services.client.impl.request.AutoPauseAndResumeConfigRequest;
+import androidx.health.services.client.impl.request.BatchingModeConfigRequest;
 import androidx.health.services.client.impl.request.CapabilitiesRequest;
 import androidx.health.services.client.impl.request.FlushRequest;
 import androidx.health.services.client.impl.request.ExerciseGoalRequest;
@@ -31,7 +32,7 @@
 /**
  * Interface to make ipc calls for health services exercise api.
  *
- * The next method added to the interface should use ID: 17
+ * The next method added to the interface should use ID: 18
  * (this id needs to be incremented for each added method)
  *
  * @hide
@@ -42,7 +43,7 @@
      * method is added.
      *
      */
-    const int API_VERSION = 3;
+    const int API_VERSION = 4;
 
     /**
      * Returns version of this AIDL interface.
@@ -122,6 +123,13 @@
     void overrideAutoPauseAndResumeForActiveExercise(in AutoPauseAndResumeConfigRequest request, IStatusCallback statusCallback) = 10;
 
     /**
+     * Sets batching mode for an active exercise.
+     *
+     * <p>Added in API version 4.
+     */
+    void overrideBatchingModesForActiveExercise(in BatchingModeConfigRequest request, IStatusCallback statusCallback) = 17;
+
+    /**
      * Method to get capabilities.
      */
     ExerciseCapabilitiesResponse getCapabilities(in CapabilitiesRequest request) = 11;
diff --git a/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/request/BatchingModeConfigRequest.aidl b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/request/BatchingModeConfigRequest.aidl
new file mode 100644
index 0000000..a8a7168
--- /dev/null
+++ b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/request/BatchingModeConfigRequest.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 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.health.services.client.impl.request;
+
+/** @hide */
+parcelable BatchingModeConfigRequest;
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
index d6e9a1b..e1b58d7 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
@@ -16,6 +16,7 @@
 
 package androidx.health.services.client
 
+import androidx.health.services.client.data.BatchingMode
 import androidx.health.services.client.data.DataPoint
 import androidx.health.services.client.data.DataType
 import androidx.health.services.client.data.ExerciseCapabilities
@@ -273,6 +274,18 @@
     ): ListenableFuture<Void>
 
     /**
+     * Sets the batching mode for the current exercise.
+     *
+     * @param batchingModes [BatchingMode] overrides for exercise updates. Passing an empty set will
+     * clear all existing overrides.
+     * @return a [ListenableFuture] that completes once the override has completed. This returned
+     * [ListenableFuture] fails if an exercise is not active for this app.
+     */
+    public fun overrideBatchingModesForActiveExerciseAsync(
+        batchingModes: Set<BatchingMode>
+    ): ListenableFuture<Void>
+
+    /**
      * Returns the [ExerciseCapabilities] of this client for the device.
      *
      * This can be used to determine what [ExerciseType]s and [DataType]s this device supports.
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/BatchingMode.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/BatchingMode.kt
new file mode 100644
index 0000000..66c4f95
--- /dev/null
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/BatchingMode.kt
@@ -0,0 +1,53 @@
+package androidx.health.services.client.data
+
+import androidx.health.services.client.proto.DataProto
+
+/**
+ * Batching mode during an active exercise when the device is in a non-interactive power state, used
+ * in [ExerciseConfig]. Not applicable when device is in interactive state because exercise updates
+ * will be streaming.
+ */
+public class BatchingMode
+internal constructor(
+    /** Unique identifier for the [BatchingMode], as an `int`. */
+    internal val id: Int,
+) {
+
+    internal constructor(
+        proto: DataProto.BatchingMode
+    ) : this(
+        proto.number,
+    )
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is BatchingMode) return false
+
+        if (id != other.id) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        return id
+    }
+
+    internal fun toProto(): DataProto.BatchingMode =
+        DataProto.BatchingMode.forNumber(id) ?: DataProto.BatchingMode.BATCHING_MODE_UNKNOWN
+
+    public companion object {
+        /**
+         * Batching mode for receiving [DataType.HEART_RATE_BPM] updates with fast frequency.
+         *
+         * Note: This mode will cause significantly increased power consumption compared to the
+         * default batching mode, while still being more power efficient than streaming when in
+         * non-interactive state. The exact power/performance tradeoff of this mode is device
+         * implementation dependent but aims for roughly five second updates.
+         */
+        @JvmField public val HEART_RATE_5_SECONDS: BatchingMode = BatchingMode(1)
+
+        @JvmStatic
+        internal fun fromProto(proto: DataProto.BatchingMode): BatchingMode =
+            BatchingMode(proto.number)
+    }
+}
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseCapabilities.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseCapabilities.kt
index d2a684e..88c7df1 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseCapabilities.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseCapabilities.kt
@@ -29,8 +29,17 @@
      * Mapping for each supported [ExerciseType] to its [ExerciseTypeCapabilities] on this device.
      */
     public val typeToCapabilities: Map<ExerciseType, ExerciseTypeCapabilities>,
+    /** Supported [BatchingMode] overrides on this device. */
+    public val supportedBatchingModeOverrides: Set<BatchingMode> = emptySet(),
 ) {
 
+    constructor(
+        typeToCapabilities: Map<ExerciseType, ExerciseTypeCapabilities>
+    ) : this(
+        typeToCapabilities,
+        emptySet()
+    )
+
     internal constructor(
         proto: DataProto.ExerciseCapabilities
     ) : this(
@@ -39,7 +48,8 @@
             .map { entry ->
                 ExerciseType.fromProto(entry.type) to ExerciseTypeCapabilities(entry.capabilities)
             }
-            .toMap()
+            .toMap(),
+        proto.supportedBatchingModeOverridesList.map { BatchingMode(it) }.toSet(),
     )
 
     internal val proto: DataProto.ExerciseCapabilities =
@@ -54,6 +64,9 @@
                     }
                     .sortedBy { it.type.name } // Ensures equals() works correctly
             )
+            .addAllSupportedBatchingModeOverrides(
+                supportedBatchingModeOverrides.map { it.toProto() }
+            )
             .build()
 
     /** Set of supported [ExerciseType] s on this device. */
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt
index ae694c1..9110a8d 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt
@@ -41,6 +41,7 @@
  * this exercise
  * @property exerciseTypeConfig [ExerciseTypeConfig] containing attributes which may be
  * modified after the exercise has started
+ * @property batchingModeOverrides [BatchingMode] overrides for this exercise
  */
 @Suppress("ParcelCreator")
 class ExerciseConfig(
@@ -51,7 +52,8 @@
     val exerciseGoals: List<ExerciseGoal<*>> = listOf(),
     val exerciseParams: Bundle = Bundle(),
     @FloatRange(from = 0.0) val swimmingPoolLengthMeters: Float = SWIMMING_POOL_LENGTH_UNSPECIFIED,
-    val exerciseTypeConfig: ExerciseTypeConfig? = null
+    val exerciseTypeConfig: ExerciseTypeConfig? = null,
+    val batchingModeOverrides: Set<BatchingMode> = emptySet(),
 ) {
     constructor(
         exerciseType: ExerciseType,
@@ -61,6 +63,27 @@
         exerciseGoals: List<ExerciseGoal<*>> = listOf(),
         exerciseParams: Bundle = Bundle(),
         @FloatRange(from = 0.0) swimmingPoolLengthMeters: Float = SWIMMING_POOL_LENGTH_UNSPECIFIED,
+        exerciseTypeConfig: ExerciseTypeConfig? = null,
+    ) : this(
+        exerciseType,
+        dataTypes,
+        isAutoPauseAndResumeEnabled,
+        isGpsEnabled,
+        exerciseGoals,
+        exerciseParams,
+        swimmingPoolLengthMeters,
+        exerciseTypeConfig,
+        emptySet()
+    )
+
+    constructor(
+        exerciseType: ExerciseType,
+        dataTypes: Set<DataType<*, *>>,
+        isAutoPauseAndResumeEnabled: Boolean,
+        isGpsEnabled: Boolean,
+        exerciseGoals: List<ExerciseGoal<*>> = listOf(),
+        exerciseParams: Bundle = Bundle(),
+        @FloatRange(from = 0.0) swimmingPoolLengthMeters: Float = SWIMMING_POOL_LENGTH_UNSPECIFIED,
     ) : this(
             exerciseType,
             dataTypes,
@@ -89,7 +112,8 @@
         },
         if (proto.hasExerciseTypeConfig()) {
             ExerciseTypeConfig.fromProto(proto.exerciseTypeConfig)
-        } else null
+        } else null,
+        proto.batchingModeOverridesList.map { BatchingMode(it) }.toSet(),
     )
 
     init {
@@ -124,6 +148,7 @@
         private var exerciseParams: Bundle = Bundle.EMPTY
         private var swimmingPoolLength: Float = SWIMMING_POOL_LENGTH_UNSPECIFIED
         private var exerciseTypeConfig: ExerciseTypeConfig? = null
+        private var batchingModeOverrides: Set<BatchingMode> = emptySet()
 
         /**
          * Sets the requested [DataType]s that should be tracked during this exercise. If not
@@ -213,6 +238,16 @@
             return this
         }
 
+        /**
+         * Sets the [BatchingMode] overrides for the ongoing exercise.
+         *
+         * @param batchingModeOverrides [BatchingMode] overrides
+         */
+        fun setBatchingModeOverrides(batchingModeOverrides: Set<BatchingMode>): Builder {
+            this.batchingModeOverrides = batchingModeOverrides
+            return this
+        }
+
         /** Returns the built [ExerciseConfig]. */
         fun build(): ExerciseConfig {
             return ExerciseConfig(
@@ -223,7 +258,8 @@
                 exerciseGoals,
                 exerciseParams,
                 swimmingPoolLength,
-                exerciseTypeConfig
+                exerciseTypeConfig,
+                batchingModeOverrides,
             )
         }
     }
@@ -248,6 +284,7 @@
             .addAllExerciseGoals(exerciseGoals.map { it.proto })
             .setExerciseParams(BundlesUtil.toProto(exerciseParams))
             .setSwimmingPoolLength(swimmingPoolLengthMeters)
+            .addAllBatchingModeOverrides(batchingModeOverrides.map { it.toProto() })
         if (exerciseTypeConfig != null) {
             builder.exerciseTypeConfig = exerciseTypeConfig.toProto()
         }
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt
index 9f92ad7..bfd3d63 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt
@@ -22,6 +22,7 @@
 import androidx.core.content.ContextCompat
 import androidx.health.services.client.ExerciseClient
 import androidx.health.services.client.ExerciseUpdateCallback
+import androidx.health.services.client.data.BatchingMode
 import androidx.health.services.client.data.DataType
 import androidx.health.services.client.data.ExerciseCapabilities
 import androidx.health.services.client.data.ExerciseConfig
@@ -38,6 +39,7 @@
 import androidx.health.services.client.impl.ipc.ClientConfiguration
 import androidx.health.services.client.impl.ipc.internal.ConnectionManager
 import androidx.health.services.client.impl.request.AutoPauseAndResumeConfigRequest
+import androidx.health.services.client.impl.request.BatchingModeConfigRequest
 import androidx.health.services.client.impl.request.CapabilitiesRequest
 import androidx.health.services.client.impl.request.ExerciseGoalRequest
 import androidx.health.services.client.impl.request.FlushRequest
@@ -211,6 +213,20 @@
         )
     }
 
+    override fun overrideBatchingModesForActiveExerciseAsync(
+        batchingModes: Set<BatchingMode>
+    ): ListenableFuture<Void> {
+        return executeWithVersionCheck(
+            { service, resultFuture ->
+                service.overrideBatchingModesForActiveExercise(
+                    BatchingModeConfigRequest(packageName, batchingModes),
+                    StatusCallback(resultFuture)
+                )
+            },
+            /* minApiVersion= */ 4
+        )
+    }
+
     override fun getCapabilitiesAsync(): ListenableFuture<ExerciseCapabilities> =
         Futures.transform(
             execute { service -> service.getCapabilities(CapabilitiesRequest(packageName)) },
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/BatchingModeConfigRequest.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/BatchingModeConfigRequest.kt
new file mode 100644
index 0000000..6179e7b
--- /dev/null
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/BatchingModeConfigRequest.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.services.client.impl.request
+
+import android.os.Parcelable
+import androidx.annotation.RestrictTo
+import androidx.health.services.client.data.BatchingMode
+import androidx.health.services.client.data.ProtoParcelable
+import androidx.health.services.client.proto.RequestsProto
+
+/**
+ * Request for updating batching mode of an exercise.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class BatchingModeConfigRequest(
+    public val packageName: String,
+    public val batchingModeOverrides: Set<BatchingMode>,
+) : ProtoParcelable<RequestsProto.BatchingModeConfigRequest>() {
+
+    override val proto: RequestsProto.BatchingModeConfigRequest =
+        RequestsProto.BatchingModeConfigRequest.newBuilder()
+            .setPackageName(packageName)
+            .addAllBatchingModeOverrides(batchingModeOverrides.map { it.toProto() })
+            .build()
+
+    public companion object {
+        @JvmField
+        public val CREATOR: Parcelable.Creator<BatchingModeConfigRequest> = newCreator {
+            val request = RequestsProto.BatchingModeConfigRequest.parseFrom(it)
+            BatchingModeConfigRequest(
+                request.packageName,
+                request.batchingModeOverridesList.map { BatchingMode(it) }.toSet(),
+            )
+        }
+    }
+}
diff --git a/health/health-services-client/src/main/proto/data.proto b/health/health-services-client/src/main/proto/data.proto
index 65e2287..f754276 100644
--- a/health/health-services-client/src/main/proto/data.proto
+++ b/health/health-services-client/src/main/proto/data.proto
@@ -170,6 +170,13 @@
     reserved 3 to max;  // Next ID
   }
   repeated TypeToCapabilitiesEntry type_to_capabilities = 1;
+  repeated BatchingMode supported_batching_mode_overrides = 2 [packed = true];
+  reserved 3 to max;  // Next ID
+}
+
+enum BatchingMode {
+  BATCHING_MODE_UNKNOWN = 0;
+  BATCHING_MODE_HEART_RATE_5_SECONDS = 1;
   reserved 2 to max;  // Next ID
 }
 
@@ -196,8 +203,9 @@
   optional Bundle exercise_params = 7; // TODO(b/241015676): Deprecate
   optional float swimming_pool_length = 8;
   optional ExerciseTypeConfig exercise_type_config = 10;
+  repeated BatchingMode batching_mode_overrides = 11 [packed = true];
   reserved 9;
-  reserved 11 to max;  // Next ID
+  reserved 12 to max;  // Next ID
 }
 
 message ExerciseInfo {
diff --git a/health/health-services-client/src/main/proto/requests.proto b/health/health-services-client/src/main/proto/requests.proto
index 39e0b7c..0a6d865 100644
--- a/health/health-services-client/src/main/proto/requests.proto
+++ b/health/health-services-client/src/main/proto/requests.proto
@@ -35,6 +35,12 @@
   reserved 2 to max;  // Next ID
 }
 
+message BatchingModeConfigRequest {
+  optional string package_name = 1;
+  repeated BatchingMode batching_mode_overrides = 2 [packed = true];
+  reserved 3 to max;  // Next ID
+}
+
 // Request for capabilities.
 message CapabilitiesRequest {
   optional string package_name = 1;
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt
index c61b936..fb50f7c 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt
@@ -47,6 +47,7 @@
 import androidx.health.services.client.impl.ipc.ClientConfiguration
 import androidx.health.services.client.impl.ipc.internal.ConnectionManager
 import androidx.health.services.client.impl.request.AutoPauseAndResumeConfigRequest
+import androidx.health.services.client.impl.request.BatchingModeConfigRequest
 import androidx.health.services.client.impl.request.CapabilitiesRequest
 import androidx.health.services.client.impl.request.ExerciseGoalRequest
 import androidx.health.services.client.impl.request.FlushRequest
@@ -61,6 +62,7 @@
 import com.google.common.collect.ImmutableSet
 import com.google.common.truth.Truth
 import java.util.concurrent.CancellationException
+import kotlin.test.assertFailsWith
 import kotlinx.coroutines.async
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.TestScope
@@ -755,6 +757,17 @@
         Truth.assertThat(service.exerciseConfig?.exerciseTypeConfig).isEqualTo(exerciseTypeConfig)
     }
 
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun overrideBatchingModesForActiveExercise_notImplementedError() = runTest {
+        var request: BatchingModeConfigRequest?
+        request = null
+        assertFailsWith(
+            exceptionClass = NotImplementedError::class,
+            block = { service.overrideBatchingModesForActiveExercise(request, null) }
+        )
+    }
+
     class FakeExerciseUpdateCallback : ExerciseUpdateCallback {
         val availabilities = mutableMapOf<DataType<*, *>, Availability>()
         val registrationFailureThrowables = mutableListOf<Throwable>()
@@ -917,6 +930,13 @@
             throw NotImplementedError()
         }
 
+        override fun overrideBatchingModesForActiveExercise(
+            batchingModeConfigRequest: BatchingModeConfigRequest?,
+            statuscallback: IStatusCallback?
+        ) {
+            throw NotImplementedError()
+        }
+
         override fun getCapabilities(request: CapabilitiesRequest): ExerciseCapabilitiesResponse {
             if (throwException) {
                 throw RemoteException("Remote Exception")
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseConfigTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseConfigTest.kt
index 1dba2cf..da70783e 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseConfigTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseConfigTest.kt
@@ -46,7 +46,8 @@
             exerciseTypeConfig = GolfExerciseTypeConfig(
                 GolfExerciseTypeConfig
                     .GolfShotTrackingPlaceInfo.GOLF_SHOT_TRACKING_PLACE_INFO_FAIRWAY
-            )
+            ),
+            batchingModeOverrides = setOf(BatchingMode.HEART_RATE_5_SECONDS),
         ).toProto()
 
         val config = ExerciseConfig(proto)
@@ -67,10 +68,11 @@
             .golfShotTrackingPlaceInfo
         ).isEqualTo(GolfExerciseTypeConfig
             .GolfShotTrackingPlaceInfo.GOLF_SHOT_TRACKING_PLACE_INFO_FAIRWAY)
+        assertThat(config.batchingModeOverrides).containsExactly(BatchingMode.HEART_RATE_5_SECONDS)
     }
 
     @Test
-    fun exerciseTypeConfigNull_protoRoundTrip() {
+    fun protoRoundTrip_emptyExerciseTypeConfigAndBatchingModes() {
         val proto = ExerciseConfig(
             ExerciseType.RUNNING,
             setOf(LOCATION, DISTANCE_TOTAL, HEART_RATE_BPM),
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt
index 0743bbb..a0c580b 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt
@@ -37,6 +37,7 @@
 import androidx.health.services.client.impl.internal.IStatusCallback
 import androidx.health.services.client.impl.ipc.internal.ConnectionManager
 import androidx.health.services.client.impl.request.AutoPauseAndResumeConfigRequest
+import androidx.health.services.client.impl.request.BatchingModeConfigRequest
 import androidx.health.services.client.impl.request.CapabilitiesRequest
 import androidx.health.services.client.impl.request.ExerciseGoalRequest
 import androidx.health.services.client.impl.request.FlushRequest
@@ -326,6 +327,13 @@
             throw NotImplementedError()
         }
 
+        override fun overrideBatchingModesForActiveExercise(
+            request: BatchingModeConfigRequest?,
+            statusCallback: IStatusCallback?
+        ) {
+            throw NotImplementedError()
+        }
+
         override fun getCapabilities(request: CapabilitiesRequest?): ExerciseCapabilitiesResponse {
             throw NotImplementedError()
         }
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 2978fe7..49c571e 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,35 +1,35 @@
 [versions]
-ACTIVITY = "1.7.0-alpha03"
+ACTIVITY = "1.7.0-alpha05"
 ADS_IDENTIFIER = "1.0.0-alpha05"
-ANNOTATION = "1.6.0-alpha01"
+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-alpha01"
+ARCH_CORE = "2.2.0-rc01"
 ASYNCLAYOUTINFLATER = "1.1.0-alpha02"
 AUTOFILL = "1.2.0-beta02"
-BENCHMARK = "1.2.0-alpha09"
+BENCHMARK = "1.2.0-alpha10"
 BIOMETRIC = "1.2.0-alpha06"
 BLUETOOTH = "1.0.0-alpha01"
-BROWSER = "1.5.0-beta01"
+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-alpha04"
-COMPOSE_COMPILER = "1.4.0-alpha02"
-COMPOSE_MATERIAL3 = "1.1.0-alpha04"
+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-alpha01"
+CORE = "1.10.0-alpha03"
 CORE_ANIMATION = "1.0.0-beta02"
 CORE_ANIMATION_TESTING = "1.0.0-beta01"
 CORE_APPDIGEST = "1.0.0-alpha01"
@@ -41,7 +41,7 @@
 CORE_ROLE = "1.2.0-alpha01"
 CORE_SPLASHSCREEN = "1.1.0-alpha01"
 CORE_UWB = "1.0.0-alpha05"
-CREDENTIALS = "1.0.0-alpha01"
+CREDENTIALS = "1.0.0-alpha02"
 CURSORADAPTER = "1.1.0-alpha01"
 CUSTOMVIEW = "1.2.0-alpha03"
 CUSTOMVIEW_POOLINGCONTAINER = "1.1.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-alpha01"
+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,8 +64,8 @@
 GRAPHICS = "1.0.0-alpha03"
 GRAPHICS_FILTERS = "1.0.0-alpha01"
 GRIDLAYOUT = "1.1.0-alpha01"
-HEALTH_CONNECT = "1.0.0-alpha09"
-HEALTH_SERVICES_CLIENT = "1.0.0-beta02"
+HEALTH_CONNECT = "1.0.0-alpha11"
+HEALTH_SERVICES_CLIENT = "1.0.0-beta03"
 HEIFWRITER = "1.1.0-alpha02"
 HILT = "1.1.0-alpha02"
 HILT_NAVIGATION_COMPOSE = "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-alpha04"
+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-alpha02"
+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-alpha03"
+PROFILEINSTALLER = "1.3.0-beta01"
 RECOMMENDATION = "1.1.0-alpha01"
 RECYCLERVIEW = "1.4.0-alpha01"
 RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
@@ -117,14 +119,14 @@
 SLIDINGPANELAYOUT = "1.3.0-alpha01"
 SQLITE = "2.4.0-alpha01"
 SQLITE_INSPECTOR = "2.1.0-alpha01"
-STARTUP = "1.2.0-alpha02"
+STARTUP = "1.2.0-alpha03"
 SWIPEREFRESHLAYOUT = "1.2.0-alpha01"
 TESTEXT = "1.0.0-alpha01"
 TESTSCREENSHOT = "1.0.0-alpha01"
-TEST_UIAUTOMATOR = "2.3.0-alpha02"
+TEST_UIAUTOMATOR = "2.3.0-alpha03"
 TEXT = "1.0.0-alpha01"
 TRACING = "1.2.0-alpha02"
-TRACING_PERFETTO = "1.0.0-alpha09"
+TRACING_PERFETTO = "1.0.0-alpha10"
 TRANSITION = "1.5.0-alpha01"
 TV = "1.0.0-alpha04"
 TVPROVIDER = "1.1.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-alpha02"
+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-alpha01"
+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-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/api/current.ignore b/lifecycle/lifecycle-livedata-ktx/api/current.ignore
new file mode 100644
index 0000000..a088b86
--- /dev/null
+++ b/lifecycle/lifecycle-livedata-ktx/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.lifecycle.TransformationsKt:
+    Removed class androidx.lifecycle.TransformationsKt
diff --git a/lifecycle/lifecycle-livedata-ktx/api/current.txt b/lifecycle/lifecycle-livedata-ktx/api/current.txt
index 7bf189f..bae0928 100644
--- a/lifecycle/lifecycle-livedata-ktx/api/current.txt
+++ b/lifecycle/lifecycle-livedata-ktx/api/current.txt
@@ -21,11 +21,5 @@
     property public abstract T? latestValue;
   }
 
-  public final class TransformationsKt {
-    method @CheckResult public static inline <X> androidx.lifecycle.LiveData<X> distinctUntilChanged(androidx.lifecycle.LiveData<X>);
-    method @CheckResult public static inline <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<? super X,? extends Y> transform);
-    method @CheckResult public static inline <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<? super X,? extends androidx.lifecycle.LiveData<Y>> transform);
-  }
-
 }
 
diff --git a/lifecycle/lifecycle-livedata-ktx/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-livedata-ktx/api/public_plus_experimental_current.txt
index 7bf189f..bae0928 100644
--- a/lifecycle/lifecycle-livedata-ktx/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-livedata-ktx/api/public_plus_experimental_current.txt
@@ -21,11 +21,5 @@
     property public abstract T? latestValue;
   }
 
-  public final class TransformationsKt {
-    method @CheckResult public static inline <X> androidx.lifecycle.LiveData<X> distinctUntilChanged(androidx.lifecycle.LiveData<X>);
-    method @CheckResult public static inline <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<? super X,? extends Y> transform);
-    method @CheckResult public static inline <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<? super X,? extends androidx.lifecycle.LiveData<Y>> transform);
-  }
-
 }
 
diff --git a/lifecycle/lifecycle-livedata-ktx/api/restricted_current.ignore b/lifecycle/lifecycle-livedata-ktx/api/restricted_current.ignore
new file mode 100644
index 0000000..a088b86
--- /dev/null
+++ b/lifecycle/lifecycle-livedata-ktx/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedClass: androidx.lifecycle.TransformationsKt:
+    Removed class androidx.lifecycle.TransformationsKt
diff --git a/lifecycle/lifecycle-livedata-ktx/api/restricted_current.txt b/lifecycle/lifecycle-livedata-ktx/api/restricted_current.txt
index 7bf189f..bae0928 100644
--- a/lifecycle/lifecycle-livedata-ktx/api/restricted_current.txt
+++ b/lifecycle/lifecycle-livedata-ktx/api/restricted_current.txt
@@ -21,11 +21,5 @@
     property public abstract T? latestValue;
   }
 
-  public final class TransformationsKt {
-    method @CheckResult public static inline <X> androidx.lifecycle.LiveData<X> distinctUntilChanged(androidx.lifecycle.LiveData<X>);
-    method @CheckResult public static inline <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<? super X,? extends Y> transform);
-    method @CheckResult public static inline <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<? super X,? extends androidx.lifecycle.LiveData<Y>> transform);
-  }
-
 }
 
diff --git a/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/Transformations.kt b/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/Transformations.kt
deleted file mode 100644
index 3e22243..0000000
--- a/lifecycle/lifecycle-livedata-ktx/src/main/java/androidx/lifecycle/Transformations.kt
+++ /dev/null
@@ -1,92 +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 androidx.annotation.CheckResult
-
-/**
- * Returns a [LiveData] mapped from `this` LiveData by applying [transform] to each value set on
- * `this` LiveData.
- *
- * This method is analogous to [io.reactivex.Observable.map].
- *
- * [transform] will be executed on the main thread.
- *
- * Here is an example mapping a simple `User` struct in a `LiveData` to a
- * `LiveData` containing their full name as a `String`.
- *
- * ```
- * val userLD : LiveData<User> = ...;
- * val userFullNameLD: LiveData<String> = userLD.map { user -> user.firstName + user.lastName }
- * ```
- */
-@CheckResult
-public inline fun <X, Y> LiveData<X>.map(crossinline transform: (X) -> Y): LiveData<Y> =
-    Transformations.map(this) { transform(it) }
-
-/**
- * Returns a [LiveData] mapped from the input `this` `LiveData` by applying
- * [transform] to each value set on `this`.
- * <p>
- * The returned `LiveData` delegates to the most recent `LiveData` created by
- * [transform] with the most recent value set to `this`, without
- * changing the reference. In this way [transform] can change the 'backing'
- * `LiveData` transparently to any observer registered to the `LiveData` returned
- * by `switchMap()`.
- *
- * Note that when the backing `LiveData` is switched, no further values from the older
- * `LiveData` will be set to the output `LiveData`. In this way, the method is
- * analogous to [io.reactivex.Observable.switchMap].
- *
- * [transform] will be executed on the main thread.
- *
- * Here is an example class that holds a typed-in name of a user
- * `String` (such as from an `EditText`) in a [MutableLiveData] and
- * returns a `LiveData` containing a List of `User` objects for users that have
- * that name. It populates that `LiveData` by requerying a repository-pattern object
- * each time the typed name changes.
- * <p>
- * This `ViewModel` would permit the observing UI to update "live" as the user ID text
- * changes.
- *
- * ```
- * class UserViewModel: AndroidViewModel {
- *     val nameQueryLiveData : MutableLiveData<String> = ...
- *
- *     fun usersWithNameLiveData(): LiveData<List<String>> = nameQueryLiveData.switchMap {
- *         name -> myDataSource.usersWithNameLiveData(name)
- *     }
- *
- *     fun setNameQuery(val name: String) {
- *         this.nameQueryLiveData.value = name;
- *     }
- * }
- * ```
- */
-@CheckResult
-public inline fun <X, Y> LiveData<X>.switchMap(
-    crossinline transform: (X) -> LiveData<Y>?
-): LiveData<Y> = Transformations.switchMap(this) { transform(it) }
-
-/**
- * Creates a new [LiveData] object does not emit a value until the source `this` LiveData value
- * has been changed.  The value is considered changed if `equals()` yields `false`.
- */
-@CheckResult
-@Suppress("NOTHING_TO_INLINE")
-public inline fun <X> LiveData<X>.distinctUntilChanged(): LiveData<X> =
-    Transformations.distinctUntilChanged(this)
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-ktx/src/test/java/androidx/lifecycle/TransformationsTest.kt b/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/TransformationsTest.kt
deleted file mode 100644
index e700a4b..0000000
--- a/lifecycle/lifecycle-livedata-ktx/src/test/java/androidx/lifecycle/TransformationsTest.kt
+++ /dev/null
@@ -1,83 +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 androidx.arch.core.executor.testing.InstantTaskExecutorRule
-import androidx.lifecycle.testing.TestLifecycleOwner
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import org.junit.Rule
-import org.junit.Test
-
-@Suppress("DEPRECATION")
-class TransformationsTest {
-
-    @get:Rule
-    val mInstantTaskExecutorRule = InstantTaskExecutorRule()
-
-    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
-    private val lifecycleOwner = TestLifecycleOwner(
-        coroutineDispatcher = UnconfinedTestDispatcher()
-    )
-
-    @Test fun map() {
-        val source = MutableLiveData<String>()
-        val mapped = source.map { input -> input.length }
-        var receivedValue = -1
-        mapped.observe<Int>(lifecycleOwner) { receivedValue = it }
-        source.value = "four"
-        assertThat(receivedValue).isEqualTo(4)
-    }
-
-    @Test fun switchMap() {
-        val trigger = MutableLiveData<Int>()
-        val first = MutableLiveData<String>()
-        val second = MutableLiveData<String>()
-        val result = trigger.switchMap { input -> if (input == 1) first else second }
-
-        var receivedValue = ""
-        result.observe<String>(lifecycleOwner) { receivedValue = it }
-        first.value = "first"
-        trigger.value = 1
-        second.value = "second"
-        assertThat(receivedValue).isEqualTo("first")
-        trigger.value = 2
-        assertThat(receivedValue).isEqualTo("second")
-        first.value = "failure"
-        assertThat(receivedValue).isEqualTo("second")
-    }
-
-    @Test fun distinctUntilChanged() {
-        val originalLiveData = MutableLiveData<String>()
-        val dedupedLiveData = originalLiveData.distinctUntilChanged()
-
-        var counter = 0
-        dedupedLiveData.observe<String>(lifecycleOwner) { counter++ }
-        assertThat(counter).isEqualTo(0)
-
-        originalLiveData.value = "new value"
-        assertThat(dedupedLiveData.value).isEqualTo("new value")
-        assertThat(counter).isEqualTo(1)
-
-        originalLiveData.value = "new value"
-        assertThat(counter).isEqualTo(1)
-
-        originalLiveData.value = "newer value"
-        assertThat(dedupedLiveData.value).isEqualTo("newer value")
-        assertThat(counter).isEqualTo(2)
-    }
-}
diff --git a/lifecycle/lifecycle-livedata/api/current.ignore b/lifecycle/lifecycle-livedata/api/current.ignore
new file mode 100644
index 0000000..f3f2baa
--- /dev/null
+++ b/lifecycle/lifecycle-livedata/api/current.ignore
@@ -0,0 +1,7 @@
+// Baseline format: 1.0
+ChangedType: androidx.lifecycle.Transformations#distinctUntilChanged(androidx.lifecycle.LiveData<X>):
+    Method androidx.lifecycle.Transformations.distinctUntilChanged has changed return type from androidx.lifecycle.LiveData<X!> to androidx.lifecycle.LiveData<X>
+ChangedType: androidx.lifecycle.Transformations#map(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,Y>):
+    Method androidx.lifecycle.Transformations.map has changed return type from androidx.lifecycle.LiveData<Y!> to androidx.lifecycle.LiveData<Y>
+ChangedType: androidx.lifecycle.Transformations#switchMap(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,androidx.lifecycle.LiveData<Y>>):
+    Method androidx.lifecycle.Transformations.switchMap has changed return type from androidx.lifecycle.LiveData<Y!> to androidx.lifecycle.LiveData<Y>
diff --git a/lifecycle/lifecycle-livedata/api/current.txt b/lifecycle/lifecycle-livedata/api/current.txt
index 2d5345c..9b1bf6c 100644
--- a/lifecycle/lifecycle-livedata/api/current.txt
+++ b/lifecycle/lifecycle-livedata/api/current.txt
@@ -8,10 +8,12 @@
     method @MainThread public <S> void removeSource(androidx.lifecycle.LiveData<S!>);
   }
 
-  public class Transformations {
-    method @MainThread public static <X> androidx.lifecycle.LiveData<X!> distinctUntilChanged(androidx.lifecycle.LiveData<X!>);
-    method @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y!> map(androidx.lifecycle.LiveData<X!>, androidx.arch.core.util.Function<X!,Y!>);
-    method @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y!> switchMap(androidx.lifecycle.LiveData<X!>, androidx.arch.core.util.Function<X!,androidx.lifecycle.LiveData<Y!>!>);
+  public final class Transformations {
+    method @CheckResult @MainThread public static <X> androidx.lifecycle.LiveData<X> distinctUntilChanged(androidx.lifecycle.LiveData<X>);
+    method @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<X,Y> transform);
+    method @Deprecated @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,Y> mapFunction);
+    method @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<X,androidx.lifecycle.LiveData<Y>> transform);
+    method @Deprecated @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,androidx.lifecycle.LiveData<Y>> switchMapFunction);
   }
 
 }
diff --git a/lifecycle/lifecycle-livedata/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-livedata/api/public_plus_experimental_current.txt
index 2d5345c..9b1bf6c 100644
--- a/lifecycle/lifecycle-livedata/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-livedata/api/public_plus_experimental_current.txt
@@ -8,10 +8,12 @@
     method @MainThread public <S> void removeSource(androidx.lifecycle.LiveData<S!>);
   }
 
-  public class Transformations {
-    method @MainThread public static <X> androidx.lifecycle.LiveData<X!> distinctUntilChanged(androidx.lifecycle.LiveData<X!>);
-    method @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y!> map(androidx.lifecycle.LiveData<X!>, androidx.arch.core.util.Function<X!,Y!>);
-    method @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y!> switchMap(androidx.lifecycle.LiveData<X!>, androidx.arch.core.util.Function<X!,androidx.lifecycle.LiveData<Y!>!>);
+  public final class Transformations {
+    method @CheckResult @MainThread public static <X> androidx.lifecycle.LiveData<X> distinctUntilChanged(androidx.lifecycle.LiveData<X>);
+    method @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<X,Y> transform);
+    method @Deprecated @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,Y> mapFunction);
+    method @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<X,androidx.lifecycle.LiveData<Y>> transform);
+    method @Deprecated @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,androidx.lifecycle.LiveData<Y>> switchMapFunction);
   }
 
 }
diff --git a/lifecycle/lifecycle-livedata/api/restricted_current.ignore b/lifecycle/lifecycle-livedata/api/restricted_current.ignore
index e930dd7..43f65eb 100644
--- a/lifecycle/lifecycle-livedata/api/restricted_current.ignore
+++ b/lifecycle/lifecycle-livedata/api/restricted_current.ignore
@@ -1,3 +1,9 @@
 // Baseline format: 1.0
 ChangedType: androidx.lifecycle.ComputableLiveData#getLiveData():
     Method androidx.lifecycle.ComputableLiveData.getLiveData has changed return type from androidx.lifecycle.LiveData<T!> to androidx.lifecycle.LiveData<T>
+ChangedType: androidx.lifecycle.Transformations#distinctUntilChanged(androidx.lifecycle.LiveData<X>):
+    Method androidx.lifecycle.Transformations.distinctUntilChanged has changed return type from androidx.lifecycle.LiveData<X!> to androidx.lifecycle.LiveData<X>
+ChangedType: androidx.lifecycle.Transformations#map(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,Y>):
+    Method androidx.lifecycle.Transformations.map has changed return type from androidx.lifecycle.LiveData<Y!> to androidx.lifecycle.LiveData<Y>
+ChangedType: androidx.lifecycle.Transformations#switchMap(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,androidx.lifecycle.LiveData<Y>>):
+    Method androidx.lifecycle.Transformations.switchMap has changed return type from androidx.lifecycle.LiveData<Y!> to androidx.lifecycle.LiveData<Y>
diff --git a/lifecycle/lifecycle-livedata/api/restricted_current.txt b/lifecycle/lifecycle-livedata/api/restricted_current.txt
index e25a33f..bb61b39 100644
--- a/lifecycle/lifecycle-livedata/api/restricted_current.txt
+++ b/lifecycle/lifecycle-livedata/api/restricted_current.txt
@@ -17,10 +17,12 @@
     method @MainThread public <S> void removeSource(androidx.lifecycle.LiveData<S!>);
   }
 
-  public class Transformations {
-    method @MainThread public static <X> androidx.lifecycle.LiveData<X!> distinctUntilChanged(androidx.lifecycle.LiveData<X!>);
-    method @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y!> map(androidx.lifecycle.LiveData<X!>, androidx.arch.core.util.Function<X!,Y!>);
-    method @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y!> switchMap(androidx.lifecycle.LiveData<X!>, androidx.arch.core.util.Function<X!,androidx.lifecycle.LiveData<Y!>!>);
+  public final class Transformations {
+    method @CheckResult @MainThread public static <X> androidx.lifecycle.LiveData<X> distinctUntilChanged(androidx.lifecycle.LiveData<X>);
+    method @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<X,Y> transform);
+    method @Deprecated @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> map(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,Y> mapFunction);
+    method @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, kotlin.jvm.functions.Function1<X,androidx.lifecycle.LiveData<Y>> transform);
+    method @Deprecated @CheckResult @MainThread public static <X, Y> androidx.lifecycle.LiveData<Y> switchMap(androidx.lifecycle.LiveData<X>, androidx.arch.core.util.Function<X,androidx.lifecycle.LiveData<Y>> switchMapFunction);
   }
 
 }
diff --git a/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.java b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.java
deleted file mode 100644
index f687e65..0000000
--- a/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.java
+++ /dev/null
@@ -1,194 +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.MainThread;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.arch.core.util.Function;
-
-/**
- * Transformation methods for {@link LiveData}.
- * <p>
- * These methods permit functional composition and delegation of {@link LiveData} instances. The
- * transformations are calculated lazily, and will run only when the returned {@link LiveData} is
- * observed. Lifecycle behavior is propagated from the input {@code source} {@link LiveData} to the
- * returned one.
- */
-@SuppressWarnings("WeakerAccess")
-public class Transformations {
-
-    private Transformations() {
-    }
-
-    /**
-     * Returns a {@code LiveData} mapped from the input {@code source} {@code LiveData} by applying
-     * {@code mapFunction} to each value set on {@code source}.
-     * <p>
-     * This method is analogous to {@link io.reactivex.Observable#map}.
-     * <p>
-     * {@code transform} will be executed on the main thread.
-     * <p>
-     * Here is an example mapping a simple {@code User} struct in a {@code LiveData} to a
-     * {@code LiveData} containing their full name as a {@code String}.
-     *
-     * <pre>
-     * LiveData&lt;User&gt; userLiveData = ...;
-     * LiveData&lt;String&gt; userFullNameLiveData =
-     *     Transformations.map(
-     *         userLiveData,
-     *         user -> user.firstName + user.lastName);
-     * });
-     * </pre>
-     *
-     * @param source      the {@code LiveData} to map from
-     * @param mapFunction a function to apply to each value set on {@code source} in order to set
-     *                    it
-     *                    on the output {@code LiveData}
-     * @param <X>         the generic type parameter of {@code source}
-     * @param <Y>         the generic type parameter of the returned {@code LiveData}
-     * @return a LiveData mapped from {@code source} to type {@code <Y>} by applying
-     * {@code mapFunction} to each value set.
-     */
-    @MainThread
-    @NonNull
-    public static <X, Y> LiveData<Y> map(
-            @NonNull LiveData<X> source,
-            @NonNull final Function<X, Y> mapFunction) {
-        final MediatorLiveData<Y> result = new MediatorLiveData<>();
-        result.addSource(source, new Observer<X>() {
-            @Override
-            public void onChanged(@Nullable X x) {
-                result.setValue(mapFunction.apply(x));
-            }
-        });
-        return result;
-    }
-
-    /**
-     * Returns a {@code LiveData} mapped from the input {@code source} {@code LiveData} by applying
-     * {@code switchMapFunction} to each value set on {@code source}.
-     * <p>
-     * The returned {@code LiveData} delegates to the most recent {@code LiveData} created by
-     * calling {@code switchMapFunction} with the most recent value set to {@code source}, without
-     * changing the reference. In this way, {@code switchMapFunction} can change the 'backing'
-     * {@code LiveData} transparently to any observer registered to the {@code LiveData} returned
-     * by {@code switchMap()}.
-     * <p>
-     * Note that when the backing {@code LiveData} is switched, no further values from the older
-     * {@code LiveData} will be set to the output {@code LiveData}. In this way, the method is
-     * analogous to {@link io.reactivex.Observable#switchMap}.
-     * <p>
-     * {@code switchMapFunction} will be executed on the main thread.
-     * <p>
-     * Here is an example class that holds a typed-in name of a user
-     * {@code String} (such as from an {@code EditText}) in a {@link MutableLiveData} and
-     * returns a {@code LiveData} containing a List of {@code User} objects for users that have
-     * that name. It populates that {@code LiveData} by requerying a repository-pattern object
-     * each time the typed name changes.
-     * <p>
-     * This {@code ViewModel} would permit the observing UI to update "live" as the user ID text
-     * changes.
-     *
-     * <pre>
-     * class UserViewModel extends AndroidViewModel {
-     *     MutableLiveData&lt;String&gt; nameQueryLiveData = ...
-     *
-     *     LiveData&lt;List&lt;String&gt;&gt; getUsersWithNameLiveData() {
-     *         return Transformations.switchMap(
-     *             nameQueryLiveData,
-     *                 name -> myDataSource.getUsersWithNameLiveData(name));
-     *     }
-     *
-     *     void setNameQuery(String name) {
-     *         this.nameQueryLiveData.setValue(name);
-     *     }
-     * }
-     * </pre>
-     *
-     * @param source            the {@code LiveData} to map from
-     * @param switchMapFunction a function to apply to each value set on {@code source} to create a
-     *                          new delegate {@code LiveData} for the returned one
-     * @param <X>               the generic type parameter of {@code source}
-     * @param <Y>               the generic type parameter of the returned {@code LiveData}
-     * @return a LiveData mapped from {@code source} to type {@code <Y>} by delegating
-     * to the LiveData returned by applying {@code switchMapFunction} to each
-     * value set
-     */
-    @MainThread
-    @NonNull
-    public static <X, Y> LiveData<Y> switchMap(
-            @NonNull LiveData<X> source,
-            @NonNull final Function<X, LiveData<Y>> switchMapFunction) {
-        final MediatorLiveData<Y> result = new MediatorLiveData<>();
-        result.addSource(source, new Observer<X>() {
-            LiveData<Y> mSource;
-
-            @Override
-            public void onChanged(@Nullable X x) {
-                LiveData<Y> newLiveData = switchMapFunction.apply(x);
-                if (mSource == newLiveData) {
-                    return;
-                }
-                if (mSource != null) {
-                    result.removeSource(mSource);
-                }
-                mSource = newLiveData;
-                if (mSource != null) {
-                    result.addSource(mSource, new Observer<Y>() {
-                        @Override
-                        public void onChanged(@Nullable Y y) {
-                            result.setValue(y);
-                        }
-                    });
-                }
-            }
-        });
-        return result;
-    }
-
-    /**
-     * Creates a new {@link LiveData} object that does not emit a value until the source LiveData
-     * value has been changed.  The value is considered changed if {@code equals()} yields
-     * {@code false}.
-     *
-     * @param source the input {@link LiveData}
-     * @param <X>    the generic type parameter of {@code source}
-     * @return       a new {@link LiveData} of type {@code X}
-     */
-    @MainThread
-    @NonNull
-    public static <X> LiveData<X> distinctUntilChanged(@NonNull LiveData<X> source) {
-        final MediatorLiveData<X> outputLiveData = new MediatorLiveData<>();
-        outputLiveData.addSource(source, new Observer<X>() {
-
-            boolean mFirstTime = true;
-
-            @Override
-            public void onChanged(X currentValue) {
-                final X previousValue = outputLiveData.getValue();
-                if (mFirstTime
-                        || (previousValue == null && currentValue != null)
-                        || (previousValue != null && !previousValue.equals(currentValue))) {
-                    mFirstTime = false;
-                    outputLiveData.setValue(currentValue);
-                }
-            }
-        });
-        return outputLiveData;
-    }
-}
diff --git a/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
new file mode 100644
index 0000000..26d7c36
--- /dev/null
+++ b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/Transformations.kt
@@ -0,0 +1,195 @@
+/*
+ * 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.
+ */
+@file:JvmName("Transformations")
+
+package androidx.lifecycle
+
+import androidx.annotation.CheckResult
+import androidx.annotation.MainThread
+import androidx.arch.core.util.Function
+
+/**
+ * Returns a [LiveData] mapped from `this` LiveData by applying [transform] to each value set on
+ * `this` LiveData.
+ *
+ * This method is analogous to [io.reactivex.Observable.map].
+ *
+ * [transform] will be executed on the main thread.
+ *
+ * Here is an example mapping a simple `User` struct in a `LiveData` to a
+ * `LiveData` containing their full name as a `String`.
+ *
+ * ```
+ * val userLD : LiveData<User> = ...;
+ * val userFullNameLD: LiveData<String> = userLD.map { user -> user.firstName + user.lastName }
+ * ```
+ *
+ * @param transform a function to apply to each value set on `source` in order to set
+ *                    it on the output `LiveData`
+ * @return a LiveData mapped from `source` to type `<Y>` by applying
+ * `mapFunction` to each value set.
+ */
+@JvmName("map")
+@MainThread
+@CheckResult
+fun <X, Y> LiveData<X>.map(
+    transform: (@JvmSuppressWildcards X) -> (@JvmSuppressWildcards Y)
+): LiveData<Y> {
+    val result = MediatorLiveData<Y>()
+    result.addSource(this) { x -> result.value = transform(x) }
+    return result
+}
+
+@Deprecated(
+    "Use kotlin functions, instead of outdated arch core Functions",
+    level = DeprecationLevel.HIDDEN
+)
+@JvmName("map")
+@MainThread
+@CheckResult
+fun <X, Y> LiveData<X>.map(mapFunction: Function<X, Y>): LiveData<Y> {
+    val result = MediatorLiveData<Y>()
+    result.addSource(this) { x -> result.value = mapFunction.apply(x) }
+    return result
+}
+
+/**
+ * Returns a [LiveData] mapped from the input `this` `LiveData` by applying
+ * [transform] to each value set on `this`.
+ * <p>
+ * The returned `LiveData` delegates to the most recent `LiveData` created by
+ * [transform] with the most recent value set to `this`, without
+ * changing the reference. In this way [transform] can change the 'backing'
+ * `LiveData` transparently to any observer registered to the `LiveData` returned
+ * by `switchMap()`.
+ *
+ * Note that when the backing `LiveData` is switched, no further values from the older
+ * `LiveData` will be set to the output `LiveData`. In this way, the method is
+ * analogous to [io.reactivex.Observable.switchMap].
+ *
+ * [transform] will be executed on the main thread.
+ *
+ * Here is an example class that holds a typed-in name of a user
+ * `String` (such as from an `EditText`) in a [MutableLiveData] and
+ * returns a `LiveData` containing a List of `User` objects for users that have
+ * that name. It populates that `LiveData` by requerying a repository-pattern object
+ * each time the typed name changes.
+ * <p>
+ * This `ViewModel` would permit the observing UI to update "live" as the user ID text
+ * changes.
+ *
+ * ```
+ * class UserViewModel: AndroidViewModel {
+ *     val nameQueryLiveData : MutableLiveData<String> = ...
+ *
+ *     fun usersWithNameLiveData(): LiveData<List<String>> = nameQueryLiveData.switchMap {
+ *         name -> myDataSource.usersWithNameLiveData(name)
+ *     }
+ *
+ *     fun setNameQuery(val name: String) {
+ *         this.nameQueryLiveData.value = name;
+ *     }
+ * }
+ * ```
+ *
+ * @param transform a function to apply to each value set on `source` to create a
+ *                          new delegate `LiveData` for the returned one
+ * @return a LiveData mapped from `source` to type `<Y>` by delegating to the LiveData
+ * returned by applying `switchMapFunction` to each value set
+ */
+@JvmName("switchMap")
+@MainThread
+@CheckResult
+fun <X, Y> LiveData<X>.switchMap(
+    transform: (@JvmSuppressWildcards X) -> (@JvmSuppressWildcards LiveData<Y>)?
+): LiveData<Y> {
+    val result = MediatorLiveData<Y>()
+    result.addSource(this, object : Observer<X> {
+        var liveData: LiveData<Y>? = null
+
+        override fun onChanged(value: X) {
+            val newLiveData = transform(value)
+            if (liveData === newLiveData) {
+                return
+            }
+            if (liveData != null) {
+                result.removeSource(liveData!!)
+            }
+            liveData = newLiveData
+            if (liveData != null) {
+                result.addSource(liveData!!) { y -> result.setValue(y) }
+            }
+        }
+    })
+    return result
+}
+
+@Deprecated(
+    "Use kotlin functions, instead of outdated arch core Functions",
+    level = DeprecationLevel.HIDDEN
+)
+@JvmName("switchMap")
+@MainThread
+@CheckResult
+fun <X, Y> LiveData<X>.switchMap(switchMapFunction: Function<X, LiveData<Y>>): LiveData<Y> {
+    val result = MediatorLiveData<Y>()
+    result.addSource(this, object : Observer<X> {
+        var liveData: LiveData<Y>? = null
+
+        override fun onChanged(value: X) {
+            val newLiveData = switchMapFunction.apply(value)
+            if (liveData === newLiveData) {
+                return
+            }
+            if (liveData != null) {
+                result.removeSource(liveData!!)
+            }
+            liveData = newLiveData
+            if (liveData != null) {
+                result.addSource(liveData!!) { y -> result.setValue(y) }
+            }
+        }
+    })
+    return result
+}
+
+/**
+ * Creates a new [LiveData] object does not emit a value until the source `this` LiveData value
+ * has been changed. The value is considered changed if `equals()` yields `false`.
+ *
+ * @return a new [LiveData] of type `X`
+ */
+@JvmName("distinctUntilChanged")
+@MainThread
+@CheckResult
+fun <X> LiveData<X>.distinctUntilChanged(): LiveData<X> {
+    val outputLiveData = MediatorLiveData<X>()
+    outputLiveData.addSource(this, object : Observer<X> {
+        var firstTime = true
+
+        override fun onChanged(value: X) {
+            val previousValue = outputLiveData.value
+            if (firstTime ||
+                previousValue == null && value != null ||
+                previousValue != null && previousValue != value
+            ) {
+                firstTime = false
+                outputLiveData.value = value
+            }
+        }
+    })
+    return outputLiveData
+}
\ No newline at end of file
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 de44aa0..4b1a493 100644
--- a/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.java
+++ b/lifecycle/lifecycle-livedata/src/test/java/androidx/lifecycle/TransformationsTest.java
@@ -30,7 +30,6 @@
 
 import androidx.annotation.Nullable;
 import androidx.arch.core.executor.ArchTaskExecutor;
-import androidx.arch.core.util.Function;
 import androidx.lifecycle.testing.TestLifecycleOwner;
 import androidx.lifecycle.util.InstantTaskExecutor;
 
@@ -59,12 +58,7 @@
     @Test
     public void testMap() {
         LiveData<String> source = new MutableLiveData<>();
-        LiveData<Integer> mapped = Transformations.map(source, new Function<String, Integer>() {
-            @Override
-            public Integer apply(String input) {
-                return input.length();
-            }
-        });
+        LiveData<Integer> mapped = Transformations.map(source, String::length);
         Observer<Integer> observer = mock(Observer.class);
         mapped.observe(mOwner, observer);
         source.setValue("four");
@@ -76,15 +70,13 @@
         LiveData<Integer> trigger = new MutableLiveData<>();
         final LiveData<String> first = new MutableLiveData<>();
         final LiveData<String> second = new MutableLiveData<>();
-        LiveData<String> result = Transformations.switchMap(trigger,
-                new Function<Integer, LiveData<String>>() {
-                    @Override
-                    public LiveData<String> apply(Integer input) {
-                        if (input == 1) {
-                            return first;
-                        } else {
-                            return second;
-                        }
+        LiveData<String> result = Transformations.switchMap(
+                trigger,
+                (Integer input) -> {
+                    if (input == 1) {
+                        return first;
+                    } else {
+                        return second;
                     }
                 });
 
@@ -109,15 +101,13 @@
         LiveData<Integer> trigger = new MutableLiveData<>();
         final LiveData<String> first = new MutableLiveData<>();
         final LiveData<String> second = new MutableLiveData<>();
-        LiveData<String> result = Transformations.switchMap(trigger,
-                new Function<Integer, LiveData<String>>() {
-                    @Override
-                    public LiveData<String> apply(Integer input) {
-                        if (input == 1) {
-                            return first;
-                        } else {
-                            return second;
-                        }
+        LiveData<String> result = Transformations.switchMap(
+                trigger,
+                (Integer input) -> {
+                    if (input == 1) {
+                        return first;
+                    } else {
+                        return second;
                     }
                 });
 
@@ -146,13 +136,7 @@
     public void testNoRedispatchSwitchMap() {
         LiveData<Integer> trigger = new MutableLiveData<>();
         final LiveData<String> first = new MutableLiveData<>();
-        LiveData<String> result = Transformations.switchMap(trigger,
-                new Function<Integer, LiveData<String>>() {
-                    @Override
-                    public LiveData<String> apply(Integer input) {
-                        return first;
-                    }
-                });
+        LiveData<String> result = Transformations.switchMap(trigger, (Integer input) -> first);
 
         Observer<String> observer = mock(Observer.class);
         result.observe(mOwner, observer);
@@ -169,15 +153,13 @@
     public void testSwitchMapToNull() {
         LiveData<Integer> trigger = new MutableLiveData<>();
         final LiveData<String> first = new MutableLiveData<>();
-        LiveData<String> result = Transformations.switchMap(trigger,
-                new Function<Integer, LiveData<String>>() {
-                    @Override
-                    public LiveData<String> apply(Integer input) {
-                        if (input == 1) {
-                            return first;
-                        } else {
-                            return null;
-                        }
+        LiveData<String> result = Transformations.switchMap(
+                trigger,
+                (Integer input) -> {
+                    if (input == 1) {
+                        return first;
+                    } else {
+                        return null;
                     }
                 });
 
@@ -197,12 +179,7 @@
     @Test
     public void noObsoleteValueTest() {
         MutableLiveData<Integer> numbers = new MutableLiveData<>();
-        LiveData<Integer> squared = Transformations.map(numbers, new Function<Integer, Integer>() {
-            @Override
-            public Integer apply(Integer input) {
-                return input * input;
-            }
-        });
+        LiveData<Integer> squared = Transformations.map(numbers, (Integer input) -> input * input);
 
         Observer observer = mock(Observer.class);
         squared.setValue(1);
@@ -261,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.ignore b/lifecycle/lifecycle-process/api/current.ignore
index 56d4ca4..6350fca 100644
--- a/lifecycle/lifecycle-process/api/current.ignore
+++ b/lifecycle/lifecycle-process/api/current.ignore
@@ -1,3 +1,7 @@
 // Baseline format: 1.0
+AddedFinal: androidx.lifecycle.ProcessLifecycleOwner#getLifecycle():
+    Method androidx.lifecycle.ProcessLifecycleOwner.getLifecycle has added 'final' qualifier
+
+
 ChangedType: androidx.lifecycle.ProcessLifecycleInitializer#dependencies():
     Method androidx.lifecycle.ProcessLifecycleInitializer.dependencies has changed return type from java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> to java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>>
diff --git a/lifecycle/lifecycle-process/api/current.txt b/lifecycle/lifecycle-process/api/current.txt
index 572a7ab..35d5ac4 100644
--- a/lifecycle/lifecycle-process/api/current.txt
+++ b/lifecycle/lifecycle-process/api/current.txt
@@ -7,9 +7,14 @@
     method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>> dependencies();
   }
 
-  public class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
+  public final class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
     method public static androidx.lifecycle.LifecycleOwner get();
     method public androidx.lifecycle.Lifecycle getLifecycle();
+    field public static final androidx.lifecycle.ProcessLifecycleOwner.Companion Companion;
+  }
+
+  public static final class ProcessLifecycleOwner.Companion {
+    method public androidx.lifecycle.LifecycleOwner get();
   }
 
 }
diff --git a/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt
index 572a7ab..35d5ac4 100644
--- a/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt
@@ -7,9 +7,14 @@
     method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>> dependencies();
   }
 
-  public class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
+  public final class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
     method public static androidx.lifecycle.LifecycleOwner get();
     method public androidx.lifecycle.Lifecycle getLifecycle();
+    field public static final androidx.lifecycle.ProcessLifecycleOwner.Companion Companion;
+  }
+
+  public static final class ProcessLifecycleOwner.Companion {
+    method public androidx.lifecycle.LifecycleOwner get();
   }
 
 }
diff --git a/lifecycle/lifecycle-process/api/restricted_current.ignore b/lifecycle/lifecycle-process/api/restricted_current.ignore
index 56d4ca4..6350fca 100644
--- a/lifecycle/lifecycle-process/api/restricted_current.ignore
+++ b/lifecycle/lifecycle-process/api/restricted_current.ignore
@@ -1,3 +1,7 @@
 // Baseline format: 1.0
+AddedFinal: androidx.lifecycle.ProcessLifecycleOwner#getLifecycle():
+    Method androidx.lifecycle.ProcessLifecycleOwner.getLifecycle has added 'final' qualifier
+
+
 ChangedType: androidx.lifecycle.ProcessLifecycleInitializer#dependencies():
     Method androidx.lifecycle.ProcessLifecycleInitializer.dependencies has changed return type from java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> to java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>>
diff --git a/lifecycle/lifecycle-process/api/restricted_current.txt b/lifecycle/lifecycle-process/api/restricted_current.txt
index 572a7ab..35d5ac4 100644
--- a/lifecycle/lifecycle-process/api/restricted_current.txt
+++ b/lifecycle/lifecycle-process/api/restricted_current.txt
@@ -7,9 +7,14 @@
     method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>> dependencies();
   }
 
-  public class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
+  public final class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
     method public static androidx.lifecycle.LifecycleOwner get();
     method public androidx.lifecycle.Lifecycle getLifecycle();
+    field public static final androidx.lifecycle.ProcessLifecycleOwner.Companion Companion;
+  }
+
+  public static final class ProcessLifecycleOwner.Companion {
+    method public androidx.lifecycle.LifecycleOwner get();
   }
 
 }
diff --git a/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleOwner.java b/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleOwner.java
deleted file mode 100644
index 0226a4e..0000000
--- a/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleOwner.java
+++ /dev/null
@@ -1,229 +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.content.Context;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Handler;
-
-import androidx.annotation.DoNotInline;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.VisibleForTesting;
-import androidx.lifecycle.ReportFragment.ActivityInitializationListener;
-
-/**
- * Class that provides lifecycle for the whole application process.
- * <p>
- * You can consider this LifecycleOwner as the composite of all of your Activities, except that
- * {@link Lifecycle.Event#ON_CREATE} will be dispatched once and {@link Lifecycle.Event#ON_DESTROY}
- * will never be dispatched. Other lifecycle events will be dispatched with following rules:
- * ProcessLifecycleOwner will dispatch {@link Lifecycle.Event#ON_START},
- * {@link Lifecycle.Event#ON_RESUME} events, as a first activity moves through these events.
- * {@link Lifecycle.Event#ON_PAUSE}, {@link Lifecycle.Event#ON_STOP}, events will be dispatched with
- * a <b>delay</b> after a last activity
- * passed through them. This delay is long enough to guarantee that ProcessLifecycleOwner
- * won't send any events if activities are destroyed and recreated due to a
- * configuration change.
- *
- * <p>
- * It is useful for use cases where you would like to react on your app coming to the foreground or
- * going to the background and you don't need a milliseconds accuracy in receiving lifecycle
- * events.
- */
-@SuppressWarnings("WeakerAccess")
-public class ProcessLifecycleOwner implements LifecycleOwner {
-
-    @VisibleForTesting
-    static final long TIMEOUT_MS = 700; //mls
-
-    // ground truth counters
-    private int mStartedCounter = 0;
-    private int mResumedCounter = 0;
-
-    private boolean mPauseSent = true;
-    private boolean mStopSent = true;
-
-    private Handler mHandler;
-    private final LifecycleRegistry mRegistry = new LifecycleRegistry(this);
-
-    private Runnable mDelayedPauseRunnable = new Runnable() {
-        @Override
-        public void run() {
-            dispatchPauseIfNeeded();
-            dispatchStopIfNeeded();
-        }
-    };
-
-    ActivityInitializationListener mInitializationListener =
-            new ActivityInitializationListener() {
-                @Override
-                public void onCreate() {
-                }
-
-                @Override
-                public void onStart() {
-                    activityStarted();
-                }
-
-                @Override
-                public void onResume() {
-                    activityResumed();
-                }
-            };
-
-    private static final ProcessLifecycleOwner sInstance = new ProcessLifecycleOwner();
-
-    /**
-     * The LifecycleOwner for the whole application process. Note that if your application
-     * has multiple processes, this provider does not know about other processes.
-     *
-     * @return {@link LifecycleOwner} for the whole application.
-     */
-    @NonNull
-    public static LifecycleOwner get() {
-        return sInstance;
-    }
-
-    static void init(Context context) {
-        sInstance.attach(context);
-    }
-
-    void activityStarted() {
-        mStartedCounter++;
-        if (mStartedCounter == 1 && mStopSent) {
-            mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START);
-            mStopSent = false;
-        }
-    }
-
-    void activityResumed() {
-        mResumedCounter++;
-        if (mResumedCounter == 1) {
-            if (mPauseSent) {
-                mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
-                mPauseSent = false;
-            } else {
-                mHandler.removeCallbacks(mDelayedPauseRunnable);
-            }
-        }
-    }
-
-    void activityPaused() {
-        mResumedCounter--;
-        if (mResumedCounter == 0) {
-            mHandler.postDelayed(mDelayedPauseRunnable, TIMEOUT_MS);
-        }
-    }
-
-    void activityStopped() {
-        mStartedCounter--;
-        dispatchStopIfNeeded();
-    }
-
-    void dispatchPauseIfNeeded() {
-        if (mResumedCounter == 0) {
-            mPauseSent = true;
-            mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
-        }
-    }
-
-    void dispatchStopIfNeeded() {
-        if (mStartedCounter == 0 && mPauseSent) {
-            mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
-            mStopSent = true;
-        }
-    }
-
-    private ProcessLifecycleOwner() {
-    }
-
-    @SuppressWarnings("deprecation")
-    void attach(Context context) {
-        mHandler = new Handler();
-        mRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
-        Application app = (Application) context.getApplicationContext();
-        app.registerActivityLifecycleCallbacks(new EmptyActivityLifecycleCallbacks() {
-            @RequiresApi(29)
-            @Override
-            public void onActivityPreCreated(@NonNull Activity activity,
-                    @Nullable Bundle savedInstanceState) {
-                // We need the ProcessLifecycleOwner to get ON_START and ON_RESUME precisely
-                // before the first activity gets its LifecycleOwner started/resumed.
-                // The activity's LifecycleOwner gets started/resumed via an activity registered
-                // callback added in onCreate(). By adding our own activity registered callback in
-                // onActivityPreCreated(), we get our callbacks first while still having the
-                // right relative order compared to the Activity's onStart()/onResume() callbacks.
-                Api29Impl.registerActivityLifecycleCallbacks(activity,
-                        new EmptyActivityLifecycleCallbacks() {
-                            @Override
-                            public void onActivityPostStarted(@NonNull Activity activity) {
-                                activityStarted();
-                            }
-
-                            @Override
-                            public void onActivityPostResumed(@NonNull Activity activity) {
-                                activityResumed();
-                            }
-                        });
-            }
-
-            @Override
-            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
-                // Only use ReportFragment pre API 29 - after that, we can use the
-                // onActivityPostStarted and onActivityPostResumed callbacks registered in
-                // onActivityPreCreated()
-                if (Build.VERSION.SDK_INT < 29) {
-                    ReportFragment.get(activity).setProcessListener(mInitializationListener);
-                }
-            }
-
-            @Override
-            public void onActivityPaused(Activity activity) {
-                activityPaused();
-            }
-
-            @Override
-            public void onActivityStopped(Activity activity) {
-                activityStopped();
-            }
-        });
-    }
-
-    @NonNull
-    @Override
-    public Lifecycle getLifecycle() {
-        return mRegistry;
-    }
-
-    @RequiresApi(29)
-    static class Api29Impl {
-        private Api29Impl() {
-            // This class is not instantiable.
-        }
-
-        @DoNotInline
-        static void registerActivityLifecycleCallbacks(@NonNull Activity activity,
-                @NonNull Application.ActivityLifecycleCallbacks callback) {
-            activity.registerActivityLifecycleCallbacks(callback);
-        }
-    }
-}
diff --git a/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleOwner.kt b/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleOwner.kt
new file mode 100644
index 0000000..9bd8d89
--- /dev/null
+++ b/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleOwner.kt
@@ -0,0 +1,203 @@
+/*
+ * 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.content.Context
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+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.
+ *
+ * You can consider this LifecycleOwner as the composite of all of your Activities, except that
+ * [Lifecycle.Event.ON_CREATE] will be dispatched once and [Lifecycle.Event.ON_DESTROY]
+ * will never be dispatched. Other lifecycle events will be dispatched with following rules:
+ * ProcessLifecycleOwner will dispatch [Lifecycle.Event.ON_START],
+ * [Lifecycle.Event.ON_RESUME] events, as a first activity moves through these events.
+ * [Lifecycle.Event.ON_PAUSE], [Lifecycle.Event.ON_STOP], events will be dispatched with
+ * a **delay** after a last activity
+ * passed through them. This delay is long enough to guarantee that ProcessLifecycleOwner
+ * won't send any events if activities are destroyed and recreated due to a
+ * configuration change.
+ *
+ * It is useful for use cases where you would like to react on your app coming to the foreground or
+ * going to the background and you don't need a milliseconds accuracy in receiving lifecycle
+ * events.
+ */
+class ProcessLifecycleOwner private constructor() : LifecycleOwner {
+    // ground truth counters
+    private var startedCounter = 0
+    private var resumedCounter = 0
+    private var pauseSent = true
+    private var stopSent = true
+    private var handler: Handler? = null
+    private val registry = LifecycleRegistry(this)
+    private val delayedPauseRunnable = Runnable {
+        dispatchPauseIfNeeded()
+        dispatchStopIfNeeded()
+    }
+    private val initializationListener: ReportFragment.ActivityInitializationListener =
+        object : ReportFragment.ActivityInitializationListener {
+            override fun onCreate() {}
+
+            override fun onStart() {
+                activityStarted()
+            }
+
+            override fun onResume() {
+                activityResumed()
+            }
+        }
+
+    companion object {
+        @VisibleForTesting
+        internal const val TIMEOUT_MS: Long = 700 // mls
+        private val newInstance = ProcessLifecycleOwner()
+
+        /**
+         * The LifecycleOwner for the whole application process. Note that if your application
+         * has multiple processes, this provider does not know about other processes.
+         *
+         * @return [LifecycleOwner] for the whole application.
+         */
+        @JvmStatic
+        fun get(): LifecycleOwner {
+            return newInstance
+        }
+
+        @JvmStatic
+        internal fun init(context: Context) {
+            newInstance.attach(context)
+        }
+    }
+
+    internal fun activityStarted() {
+        startedCounter++
+        if (startedCounter == 1 && stopSent) {
+            registry.handleLifecycleEvent(Lifecycle.Event.ON_START)
+            stopSent = false
+        }
+    }
+
+    internal fun activityResumed() {
+        resumedCounter++
+        if (resumedCounter == 1) {
+            if (pauseSent) {
+                registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
+                pauseSent = false
+            } else {
+                handler!!.removeCallbacks(delayedPauseRunnable)
+            }
+        }
+    }
+
+    internal fun activityPaused() {
+        resumedCounter--
+        if (resumedCounter == 0) {
+            handler!!.postDelayed(delayedPauseRunnable, TIMEOUT_MS)
+        }
+    }
+
+    internal fun activityStopped() {
+        startedCounter--
+        dispatchStopIfNeeded()
+    }
+
+    internal fun dispatchPauseIfNeeded() {
+        if (resumedCounter == 0) {
+            pauseSent = true
+            registry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+        }
+    }
+
+    internal fun dispatchStopIfNeeded() {
+        if (startedCounter == 0 && pauseSent) {
+            registry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
+            stopSent = true
+        }
+    }
+
+    @Suppress("DEPRECATION")
+    internal fun attach(context: Context) {
+        handler = Handler()
+        registry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
+        val app = context.applicationContext as Application
+        app.registerActivityLifecycleCallbacks(object : EmptyActivityLifecycleCallbacks() {
+            @RequiresApi(29)
+            override fun onActivityPreCreated(
+                activity: Activity,
+                savedInstanceState: Bundle?
+            ) {
+                // We need the ProcessLifecycleOwner to get ON_START and ON_RESUME precisely
+                // before the first activity gets its LifecycleOwner started/resumed.
+                // The activity's LifecycleOwner gets started/resumed via an activity registered
+                // callback added in onCreate(). By adding our own activity registered callback in
+                // onActivityPreCreated(), we get our callbacks first while still having the
+                // right relative order compared to the Activity's onStart()/onResume() callbacks.
+                Api29Impl.registerActivityLifecycleCallbacks(activity,
+                    object : EmptyActivityLifecycleCallbacks() {
+                        override fun onActivityPostStarted(activity: Activity) {
+                            activityStarted()
+                        }
+
+                        override fun onActivityPostResumed(activity: Activity) {
+                            activityResumed()
+                        }
+                    })
+            }
+
+            override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
+                // Only use ReportFragment pre API 29 - after that, we can use the
+                // onActivityPostStarted and onActivityPostResumed callbacks registered in
+                // onActivityPreCreated()
+                if (Build.VERSION.SDK_INT < 29) {
+                    activity.reportFragment.setProcessListener(initializationListener)
+                }
+            }
+
+            override fun onActivityPaused(activity: Activity) {
+                activityPaused()
+            }
+
+            override fun onActivityStopped(activity: Activity) {
+                activityStopped()
+            }
+        })
+    }
+
+    override fun getLifecycle(): Lifecycle {
+        return registry
+    }
+
+    @RequiresApi(29)
+    internal object Api29Impl {
+        @DoNotInline
+        @JvmStatic
+        fun registerActivityLifecycleCallbacks(
+            activity: Activity,
+            callback: Application.ActivityLifecycleCallbacks
+        ) {
+            activity.registerActivityLifecycleCallbacks(callback)
+        }
+    }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt b/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt
index dbf4021..5f09eb2 100644
--- a/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt
+++ b/lifecycle/lifecycle-reactivestreams/src/main/java/androidx/lifecycle/LiveDataReactiveStreams.kt
@@ -75,7 +75,7 @@
         val subscriber: Subscriber<in T>,
         val lifecycle: LifecycleOwner,
         val liveData: LiveData<T>
-    ) : Subscription, Observer<T> {
+    ) : Subscription, Observer<T?> {
         @Volatile
         var canceled = false
 
@@ -86,18 +86,18 @@
         // used on main thread only
         var latest: T? = null
 
-        override fun onChanged(t: T?) {
+        override fun onChanged(value: T?) {
             if (canceled) {
                 return
             }
             if (requested > 0) {
                 latest = null
-                subscriber.onNext(t)
+                subscriber.onNext(value)
                 if (requested != Long.MAX_VALUE) {
                     requested--
                 }
             } else {
-                latest = t
+                latest = value
             }
         }
 
diff --git a/lifecycle/lifecycle-runtime/api/restricted_current.txt b/lifecycle/lifecycle-runtime/api/restricted_current.txt
index fbb9120..704cdb4 100644
--- a/lifecycle/lifecycle-runtime/api/restricted_current.txt
+++ b/lifecycle/lifecycle-runtime/api/restricted_current.txt
@@ -26,13 +26,27 @@
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class ReportFragment extends android.app.Fragment {
     ctor public ReportFragment();
-    method public static void injectIfNeededIn(android.app.Activity!);
-    method public void onActivityCreated(android.os.Bundle!);
+    method public static final androidx.lifecycle.ReportFragment get(android.app.Activity);
+    method public static final void injectIfNeededIn(android.app.Activity activity);
+    method public void onActivityCreated(android.os.Bundle? savedInstanceState);
     method public void onDestroy();
     method public void onPause();
     method public void onResume();
     method public void onStart();
     method public void onStop();
+    method public final void setProcessListener(androidx.lifecycle.ReportFragment.ActivityInitializationListener? processListener);
+    field public static final androidx.lifecycle.ReportFragment.Companion Companion;
+  }
+
+  public static interface ReportFragment.ActivityInitializationListener {
+    method public void onCreate();
+    method public void onResume();
+    method public void onStart();
+  }
+
+  public static final class ReportFragment.Companion {
+    method public androidx.lifecycle.ReportFragment get(android.app.Activity);
+    method public void injectIfNeededIn(android.app.Activity activity);
   }
 
   public final class ViewTreeLifecycleOwner {
diff --git a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/ReportFragment.java b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/ReportFragment.java
deleted file mode 100644
index a876708..0000000
--- a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/ReportFragment.java
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.lifecycle;
-
-import android.app.Activity;
-import android.app.Application;
-import android.os.Build;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-
-/**
- * Internal class that dispatches initialization events.
- *
- * @hide
- */
-@SuppressWarnings({"UnknownNullness", "deprecation"})
-// TODO https://issuetracker.google.com/issues/112197238
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public class ReportFragment extends android.app.Fragment {
-    private static final String REPORT_FRAGMENT_TAG = "androidx.lifecycle"
-            + ".LifecycleDispatcher.report_fragment_tag";
-
-    public static void injectIfNeededIn(Activity activity) {
-        if (Build.VERSION.SDK_INT >= 29) {
-            // On API 29+, we can register for the correct Lifecycle callbacks directly
-            LifecycleCallbacks.registerIn(activity);
-        }
-        // Prior to API 29 and to maintain compatibility with older versions of
-        // ProcessLifecycleOwner (which may not be updated when lifecycle-runtime is updated and
-        // need to support activities that don't extend from FragmentActivity from support lib),
-        // use a framework fragment to get the correct timing of Lifecycle events
-        android.app.FragmentManager manager = activity.getFragmentManager();
-        if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
-            manager.beginTransaction().add(new ReportFragment(), REPORT_FRAGMENT_TAG).commit();
-            // Hopefully, we are the first to make a transaction.
-            manager.executePendingTransactions();
-        }
-    }
-
-    @SuppressWarnings("deprecation")
-    static void dispatch(@NonNull Activity activity, @NonNull Lifecycle.Event event) {
-        if (activity instanceof LifecycleRegistryOwner) {
-            ((LifecycleRegistryOwner) activity).getLifecycle().handleLifecycleEvent(event);
-            return;
-        }
-
-        if (activity instanceof LifecycleOwner) {
-            Lifecycle lifecycle = ((LifecycleOwner) activity).getLifecycle();
-            if (lifecycle instanceof LifecycleRegistry) {
-                ((LifecycleRegistry) lifecycle).handleLifecycleEvent(event);
-            }
-        }
-    }
-
-    static ReportFragment get(Activity activity) {
-        return (ReportFragment) activity.getFragmentManager().findFragmentByTag(
-                REPORT_FRAGMENT_TAG);
-    }
-
-    private ActivityInitializationListener mProcessListener;
-
-    private void dispatchCreate(ActivityInitializationListener listener) {
-        if (listener != null) {
-            listener.onCreate();
-        }
-    }
-
-    private void dispatchStart(ActivityInitializationListener listener) {
-        if (listener != null) {
-            listener.onStart();
-        }
-    }
-
-    private void dispatchResume(ActivityInitializationListener listener) {
-        if (listener != null) {
-            listener.onResume();
-        }
-    }
-
-    @Override
-    public void onActivityCreated(Bundle savedInstanceState) {
-        super.onActivityCreated(savedInstanceState);
-        dispatchCreate(mProcessListener);
-        dispatch(Lifecycle.Event.ON_CREATE);
-    }
-
-    @Override
-    public void onStart() {
-        super.onStart();
-        dispatchStart(mProcessListener);
-        dispatch(Lifecycle.Event.ON_START);
-    }
-
-    @Override
-    public void onResume() {
-        super.onResume();
-        dispatchResume(mProcessListener);
-        dispatch(Lifecycle.Event.ON_RESUME);
-    }
-
-    @Override
-    public void onPause() {
-        super.onPause();
-        dispatch(Lifecycle.Event.ON_PAUSE);
-    }
-
-    @Override
-    public void onStop() {
-        super.onStop();
-        dispatch(Lifecycle.Event.ON_STOP);
-    }
-
-    @Override
-    public void onDestroy() {
-        super.onDestroy();
-        dispatch(Lifecycle.Event.ON_DESTROY);
-        // just want to be sure that we won't leak reference to an activity
-        mProcessListener = null;
-    }
-
-    private void dispatch(@NonNull Lifecycle.Event event) {
-        if (Build.VERSION.SDK_INT < 29) {
-            // Only dispatch events from ReportFragment on API levels prior
-            // to API 29. On API 29+, this is handled by the ActivityLifecycleCallbacks
-            // added in ReportFragment.injectIfNeededIn
-            dispatch(getActivity(), event);
-        }
-    }
-
-    void setProcessListener(ActivityInitializationListener processListener) {
-        mProcessListener = processListener;
-    }
-
-    interface ActivityInitializationListener {
-        void onCreate();
-
-        void onStart();
-
-        void onResume();
-    }
-
-    // this class isn't inlined only because we need to add a proguard rule for it (b/142778206)
-    // In addition to that registerIn method allows to avoid class verification failure,
-    // because registerActivityLifecycleCallbacks is available only since api 29.
-    @RequiresApi(29)
-    static class LifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
-
-        static void registerIn(Activity activity) {
-            activity.registerActivityLifecycleCallbacks(new LifecycleCallbacks());
-        }
-
-        @Override
-        public void onActivityCreated(@NonNull Activity activity,
-                @Nullable Bundle bundle) {
-        }
-
-        @Override
-        public void onActivityPostCreated(@NonNull Activity activity,
-                @Nullable Bundle savedInstanceState) {
-            dispatch(activity, Lifecycle.Event.ON_CREATE);
-        }
-
-        @Override
-        public void onActivityStarted(@NonNull Activity activity) {
-        }
-
-        @Override
-        public void onActivityPostStarted(@NonNull Activity activity) {
-            dispatch(activity, Lifecycle.Event.ON_START);
-        }
-
-        @Override
-        public void onActivityResumed(@NonNull Activity activity) {
-        }
-
-        @Override
-        public void onActivityPostResumed(@NonNull Activity activity) {
-            dispatch(activity, Lifecycle.Event.ON_RESUME);
-        }
-
-        @Override
-        public void onActivityPrePaused(@NonNull Activity activity) {
-            dispatch(activity, Lifecycle.Event.ON_PAUSE);
-        }
-
-        @Override
-        public void onActivityPaused(@NonNull Activity activity) {
-        }
-
-        @Override
-        public void onActivityPreStopped(@NonNull Activity activity) {
-            dispatch(activity, Lifecycle.Event.ON_STOP);
-        }
-
-        @Override
-        public void onActivityStopped(@NonNull Activity activity) {
-        }
-
-        @Override
-        public void onActivitySaveInstanceState(@NonNull Activity activity,
-                @NonNull Bundle bundle) {
-        }
-
-        @Override
-        public void onActivityPreDestroyed(@NonNull Activity activity) {
-            dispatch(activity, Lifecycle.Event.ON_DESTROY);
-        }
-
-        @Override
-        public void onActivityDestroyed(@NonNull Activity activity) {
-        }
-    }
-}
diff --git a/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/ReportFragment.kt b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/ReportFragment.kt
new file mode 100644
index 0000000..a7ae3f40
--- /dev/null
+++ b/lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/ReportFragment.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+import android.app.Activity
+import android.app.Application
+import android.os.Build
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+
+/**
+ * Internal class that dispatches initialization events.
+ *
+ * @hide
+ */
+@Suppress("DEPRECATION")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+open class ReportFragment() : android.app.Fragment() {
+    private var processListener: ActivityInitializationListener? = null
+
+    private fun dispatchCreate(listener: ActivityInitializationListener?) {
+        listener?.onCreate()
+    }
+
+    private fun dispatchStart(listener: ActivityInitializationListener?) {
+        listener?.onStart()
+    }
+
+    private fun dispatchResume(listener: ActivityInitializationListener?) {
+        listener?.onResume()
+    }
+
+    override fun onActivityCreated(savedInstanceState: Bundle?) {
+        super.onActivityCreated(savedInstanceState)
+        dispatchCreate(processListener)
+        dispatch(Lifecycle.Event.ON_CREATE)
+    }
+
+    override fun onStart() {
+        super.onStart()
+        dispatchStart(processListener)
+        dispatch(Lifecycle.Event.ON_START)
+    }
+
+    override fun onResume() {
+        super.onResume()
+        dispatchResume(processListener)
+        dispatch(Lifecycle.Event.ON_RESUME)
+    }
+
+    override fun onPause() {
+        super.onPause()
+        dispatch(Lifecycle.Event.ON_PAUSE)
+    }
+
+    override fun onStop() {
+        super.onStop()
+        dispatch(Lifecycle.Event.ON_STOP)
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        dispatch(Lifecycle.Event.ON_DESTROY)
+        // just want to be sure that we won't leak reference to an activity
+        processListener = null
+    }
+
+    private fun dispatch(event: Lifecycle.Event) {
+        if (Build.VERSION.SDK_INT < 29) {
+            // Only dispatch events from ReportFragment on API levels prior
+            // to API 29. On API 29+, this is handled by the ActivityLifecycleCallbacks
+            // added in ReportFragment.injectIfNeededIn
+            dispatch(activity, event)
+        }
+    }
+
+    fun setProcessListener(processListener: ActivityInitializationListener?) {
+        this.processListener = processListener
+    }
+
+    interface ActivityInitializationListener {
+        fun onCreate()
+        fun onStart()
+        fun onResume()
+    }
+
+    // this class isn't inlined only because we need to add a proguard rule for it (b/142778206)
+    // In addition to that registerIn method allows to avoid class verification failure,
+    // because registerActivityLifecycleCallbacks is available only since api 29.
+    @RequiresApi(29)
+    internal class LifecycleCallbacks : Application.ActivityLifecycleCallbacks {
+        override fun onActivityCreated(
+            activity: Activity,
+            bundle: Bundle?
+        ) {}
+
+        override fun onActivityPostCreated(
+            activity: Activity,
+            savedInstanceState: Bundle?
+        ) {
+            dispatch(activity, Lifecycle.Event.ON_CREATE)
+        }
+
+        override fun onActivityStarted(activity: Activity) {}
+
+        override fun onActivityPostStarted(activity: Activity) {
+            dispatch(activity, Lifecycle.Event.ON_START)
+        }
+
+        override fun onActivityResumed(activity: Activity) {}
+
+        override fun onActivityPostResumed(activity: Activity) {
+            dispatch(activity, Lifecycle.Event.ON_RESUME)
+        }
+
+        override fun onActivityPrePaused(activity: Activity) {
+            dispatch(activity, Lifecycle.Event.ON_PAUSE)
+        }
+
+        override fun onActivityPaused(activity: Activity) {}
+
+        override fun onActivityPreStopped(activity: Activity) {
+            dispatch(activity, Lifecycle.Event.ON_STOP)
+        }
+
+        override fun onActivityStopped(activity: Activity) {}
+
+        override fun onActivitySaveInstanceState(
+            activity: Activity,
+            bundle: Bundle
+        ) {}
+
+        override fun onActivityPreDestroyed(activity: Activity) {
+            dispatch(activity, Lifecycle.Event.ON_DESTROY)
+        }
+
+        override fun onActivityDestroyed(activity: Activity) {}
+
+        companion object {
+            @JvmStatic
+            fun registerIn(activity: Activity) {
+                activity.registerActivityLifecycleCallbacks(LifecycleCallbacks())
+            }
+        }
+    }
+
+    companion object {
+        private const val REPORT_FRAGMENT_TAG =
+            "androidx.lifecycle.LifecycleDispatcher.report_fragment_tag"
+
+        @JvmStatic
+        fun injectIfNeededIn(activity: Activity) {
+            if (Build.VERSION.SDK_INT >= 29) {
+                // On API 29+, we can register for the correct Lifecycle callbacks directly
+                LifecycleCallbacks.registerIn(activity)
+            }
+            // Prior to API 29 and to maintain compatibility with older versions of
+            // ProcessLifecycleOwner (which may not be updated when lifecycle-runtime is updated and
+            // need to support activities that don't extend from FragmentActivity from support lib),
+            // use a framework fragment to get the correct timing of Lifecycle events
+            val manager = activity.fragmentManager
+            if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
+                manager.beginTransaction().add(ReportFragment(), REPORT_FRAGMENT_TAG).commit()
+                // Hopefully, we are the first to make a transaction.
+                manager.executePendingTransactions()
+            }
+        }
+
+        @JvmStatic
+        internal fun dispatch(activity: Activity, event: Lifecycle.Event) {
+            if (activity is LifecycleRegistryOwner) {
+                activity.lifecycle.handleLifecycleEvent(event)
+                return
+            }
+            if (activity is LifecycleOwner) {
+                val lifecycle = (activity as LifecycleOwner).lifecycle
+                if (lifecycle is LifecycleRegistry) {
+                    lifecycle.handleLifecycleEvent(event)
+                }
+            }
+        }
+
+        @JvmStatic
+        @get:JvmName("get")
+        val Activity.reportFragment: ReportFragment
+            get() {
+                return this.fragmentManager.findFragmentByTag(
+                    REPORT_FRAGMENT_TAG
+                ) as ReportFragment
+            }
+    }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-service/api/current.ignore b/lifecycle/lifecycle-service/api/current.ignore
new file mode 100644
index 0000000..61ce0f2
--- /dev/null
+++ b/lifecycle/lifecycle-service/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedMethod: androidx.lifecycle.LifecycleService#onStart(android.content.Intent, int):
+    Removed method androidx.lifecycle.LifecycleService.onStart(android.content.Intent,int)
diff --git a/lifecycle/lifecycle-service/api/current.txt b/lifecycle/lifecycle-service/api/current.txt
index a29040d..a27283e 100644
--- a/lifecycle/lifecycle-service/api/current.txt
+++ b/lifecycle/lifecycle-service/api/current.txt
@@ -4,17 +4,17 @@
   public class LifecycleService extends android.app.Service implements androidx.lifecycle.LifecycleOwner {
     ctor public LifecycleService();
     method public androidx.lifecycle.Lifecycle getLifecycle();
-    method @CallSuper public android.os.IBinder? onBind(android.content.Intent);
-    method @CallSuper public void onStart(android.content.Intent?, int);
+    method @CallSuper public android.os.IBinder? onBind(android.content.Intent intent);
   }
 
   public class ServiceLifecycleDispatcher {
-    ctor public ServiceLifecycleDispatcher(androidx.lifecycle.LifecycleOwner);
+    ctor public ServiceLifecycleDispatcher(androidx.lifecycle.LifecycleOwner provider);
     method public androidx.lifecycle.Lifecycle getLifecycle();
     method public void onServicePreSuperOnBind();
     method public void onServicePreSuperOnCreate();
     method public void onServicePreSuperOnDestroy();
     method public void onServicePreSuperOnStart();
+    property public androidx.lifecycle.Lifecycle lifecycle;
   }
 
 }
diff --git a/lifecycle/lifecycle-service/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-service/api/public_plus_experimental_current.txt
index a29040d..a27283e 100644
--- a/lifecycle/lifecycle-service/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-service/api/public_plus_experimental_current.txt
@@ -4,17 +4,17 @@
   public class LifecycleService extends android.app.Service implements androidx.lifecycle.LifecycleOwner {
     ctor public LifecycleService();
     method public androidx.lifecycle.Lifecycle getLifecycle();
-    method @CallSuper public android.os.IBinder? onBind(android.content.Intent);
-    method @CallSuper public void onStart(android.content.Intent?, int);
+    method @CallSuper public android.os.IBinder? onBind(android.content.Intent intent);
   }
 
   public class ServiceLifecycleDispatcher {
-    ctor public ServiceLifecycleDispatcher(androidx.lifecycle.LifecycleOwner);
+    ctor public ServiceLifecycleDispatcher(androidx.lifecycle.LifecycleOwner provider);
     method public androidx.lifecycle.Lifecycle getLifecycle();
     method public void onServicePreSuperOnBind();
     method public void onServicePreSuperOnCreate();
     method public void onServicePreSuperOnDestroy();
     method public void onServicePreSuperOnStart();
+    property public androidx.lifecycle.Lifecycle lifecycle;
   }
 
 }
diff --git a/lifecycle/lifecycle-service/api/restricted_current.ignore b/lifecycle/lifecycle-service/api/restricted_current.ignore
new file mode 100644
index 0000000..61ce0f2
--- /dev/null
+++ b/lifecycle/lifecycle-service/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedMethod: androidx.lifecycle.LifecycleService#onStart(android.content.Intent, int):
+    Removed method androidx.lifecycle.LifecycleService.onStart(android.content.Intent,int)
diff --git a/lifecycle/lifecycle-service/api/restricted_current.txt b/lifecycle/lifecycle-service/api/restricted_current.txt
index a29040d..a27283e 100644
--- a/lifecycle/lifecycle-service/api/restricted_current.txt
+++ b/lifecycle/lifecycle-service/api/restricted_current.txt
@@ -4,17 +4,17 @@
   public class LifecycleService extends android.app.Service implements androidx.lifecycle.LifecycleOwner {
     ctor public LifecycleService();
     method public androidx.lifecycle.Lifecycle getLifecycle();
-    method @CallSuper public android.os.IBinder? onBind(android.content.Intent);
-    method @CallSuper public void onStart(android.content.Intent?, int);
+    method @CallSuper public android.os.IBinder? onBind(android.content.Intent intent);
   }
 
   public class ServiceLifecycleDispatcher {
-    ctor public ServiceLifecycleDispatcher(androidx.lifecycle.LifecycleOwner);
+    ctor public ServiceLifecycleDispatcher(androidx.lifecycle.LifecycleOwner provider);
     method public androidx.lifecycle.Lifecycle getLifecycle();
     method public void onServicePreSuperOnBind();
     method public void onServicePreSuperOnCreate();
     method public void onServicePreSuperOnDestroy();
     method public void onServicePreSuperOnStart();
+    property public androidx.lifecycle.Lifecycle lifecycle;
   }
 
 }
diff --git a/lifecycle/lifecycle-service/build.gradle b/lifecycle/lifecycle-service/build.gradle
index a5a9ac3..690553f 100644
--- a/lifecycle/lifecycle-service/build.gradle
+++ b/lifecycle/lifecycle-service/build.gradle
@@ -20,9 +20,11 @@
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
+    id("org.jetbrains.kotlin.android")
 }
 
 dependencies {
+    api(libs.kotlinStdlib)
     api(project(":lifecycle:lifecycle-runtime"))
 
     androidTestImplementation(libs.testExtJunit)
diff --git a/lifecycle/lifecycle-service/src/androidTest/java/androidx/lifecycle/service/TestService.java b/lifecycle/lifecycle-service/src/androidTest/java/androidx/lifecycle/service/TestService.java
index ffed642..d82a895 100644
--- a/lifecycle/lifecycle-service/src/androidTest/java/androidx/lifecycle/service/TestService.java
+++ b/lifecycle/lifecycle-service/src/androidTest/java/androidx/lifecycle/service/TestService.java
@@ -21,6 +21,7 @@
 import android.os.Binder;
 import android.os.IBinder;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.lifecycle.LifecycleEventObserver;
 import androidx.lifecycle.LifecycleService;
@@ -44,7 +45,7 @@
 
     @Nullable
     @Override
-    public IBinder onBind(Intent intent) {
+    public IBinder onBind(@NonNull Intent intent) {
         super.onBind(intent);
         return mBinder;
     }
diff --git a/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/LifecycleService.java b/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/LifecycleService.java
deleted file mode 100644
index 7440fac..0000000
--- a/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/LifecycleService.java
+++ /dev/null
@@ -1,79 +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.Service;
-import android.content.Intent;
-import android.os.IBinder;
-
-import androidx.annotation.CallSuper;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-/**
- * A Service that is also a {@link LifecycleOwner}.
- */
-public class LifecycleService extends Service implements LifecycleOwner {
-
-    private final ServiceLifecycleDispatcher mDispatcher = new ServiceLifecycleDispatcher(this);
-
-    @CallSuper
-    @Override
-    public void onCreate() {
-        mDispatcher.onServicePreSuperOnCreate();
-        super.onCreate();
-    }
-
-    @CallSuper
-    @Nullable
-    @Override
-    public IBinder onBind(@NonNull Intent intent) {
-        mDispatcher.onServicePreSuperOnBind();
-        return null;
-    }
-
-    @SuppressWarnings("deprecation")
-    @CallSuper
-    @Override
-    public void onStart(@Nullable Intent intent, int startId) {
-        mDispatcher.onServicePreSuperOnStart();
-        super.onStart(intent, startId);
-    }
-
-    // this method is added only to annotate it with @CallSuper.
-    // In usual service super.onStartCommand is no-op, but in LifecycleService
-    // it results in mDispatcher.onServicePreSuperOnStart() call, because
-    // super.onStartCommand calls onStart().
-    @CallSuper
-    @Override
-    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
-        return super.onStartCommand(intent, flags, startId);
-    }
-
-    @CallSuper
-    @Override
-    public void onDestroy() {
-        mDispatcher.onServicePreSuperOnDestroy();
-        super.onDestroy();
-    }
-
-    @Override
-    @NonNull
-    public Lifecycle getLifecycle() {
-        return mDispatcher.getLifecycle();
-    }
-}
diff --git a/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/LifecycleService.kt b/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/LifecycleService.kt
new file mode 100644
index 0000000..7754cc7
--- /dev/null
+++ b/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/LifecycleService.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.Service
+import android.content.Intent
+import android.os.IBinder
+import androidx.annotation.CallSuper
+
+/**
+ * A Service that is also a [LifecycleOwner].
+ */
+open class LifecycleService : Service(), LifecycleOwner {
+
+    private val dispatcher = ServiceLifecycleDispatcher(this)
+
+    @CallSuper
+    override fun onCreate() {
+        dispatcher.onServicePreSuperOnCreate()
+        super.onCreate()
+    }
+
+    @CallSuper
+    override fun onBind(intent: Intent): IBinder? {
+        dispatcher.onServicePreSuperOnBind()
+        return null
+    }
+
+    @Deprecated("Deprecated in Java")
+    @Suppress("DEPRECATION")
+    @CallSuper
+    override fun onStart(intent: Intent?, startId: Int) {
+        dispatcher.onServicePreSuperOnStart()
+        super.onStart(intent, startId)
+    }
+
+    // this method is added only to annotate it with @CallSuper.
+    // In usual Service, super.onStartCommand is no-op, but in LifecycleService
+    // it results in dispatcher.onServicePreSuperOnStart() call, because
+    // super.onStartCommand calls onStart().
+    @CallSuper
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        return super.onStartCommand(intent, flags, startId)
+    }
+
+    @CallSuper
+    override fun onDestroy() {
+        dispatcher.onServicePreSuperOnDestroy()
+        super.onDestroy()
+    }
+
+    override fun getLifecycle(): Lifecycle {
+        return dispatcher.lifecycle
+    }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/ServiceLifecycleDispatcher.java b/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/ServiceLifecycleDispatcher.java
deleted file mode 100644
index 2a366ba..0000000
--- a/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/ServiceLifecycleDispatcher.java
+++ /dev/null
@@ -1,111 +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.Service;
-import android.content.Intent;
-import android.os.Handler;
-
-import androidx.annotation.NonNull;
-
-/**
- * Helper class to dispatch lifecycle events for a service. Use it only if it is impossible
- * to use {@link LifecycleService}.
- */
-@SuppressWarnings("WeakerAccess")
-public class ServiceLifecycleDispatcher {
-    private final LifecycleRegistry mRegistry;
-    private final Handler mHandler;
-    private DispatchRunnable mLastDispatchRunnable;
-
-    /**
-     * @param provider {@link LifecycleOwner} for a service, usually it is a service itself
-     */
-    @SuppressWarnings("deprecation")
-    public ServiceLifecycleDispatcher(@NonNull LifecycleOwner provider) {
-        mRegistry = new LifecycleRegistry(provider);
-        mHandler = new Handler();
-    }
-
-    private void postDispatchRunnable(Lifecycle.Event event) {
-        if (mLastDispatchRunnable != null) {
-            mLastDispatchRunnable.run();
-        }
-        mLastDispatchRunnable = new DispatchRunnable(mRegistry, event);
-        mHandler.postAtFrontOfQueue(mLastDispatchRunnable);
-    }
-
-    /**
-     * Must be a first call in {@link Service#onCreate()} method, even before super.onCreate call.
-     */
-    public void onServicePreSuperOnCreate() {
-        postDispatchRunnable(Lifecycle.Event.ON_CREATE);
-    }
-
-    /**
-     * Must be a first call in {@link Service#onBind(Intent)} method, even before super.onBind
-     * call.
-     */
-    public void onServicePreSuperOnBind() {
-        postDispatchRunnable(Lifecycle.Event.ON_START);
-    }
-
-    /**
-     * Must be a first call in {@link Service#onStart(Intent, int)} or
-     * {@link Service#onStartCommand(Intent, int, int)} methods, even before
-     * a corresponding super call.
-     */
-    public void onServicePreSuperOnStart() {
-        postDispatchRunnable(Lifecycle.Event.ON_START);
-    }
-
-    /**
-     * Must be a first call in {@link Service#onDestroy()} method, even before super.OnDestroy
-     * call.
-     */
-    public void onServicePreSuperOnDestroy() {
-        postDispatchRunnable(Lifecycle.Event.ON_STOP);
-        postDispatchRunnable(Lifecycle.Event.ON_DESTROY);
-    }
-
-    /**
-     * @return {@link Lifecycle} for the given {@link LifecycleOwner}
-     */
-    @NonNull
-    public Lifecycle getLifecycle() {
-        return mRegistry;
-    }
-
-    static class DispatchRunnable implements Runnable {
-        private final LifecycleRegistry mRegistry;
-        final Lifecycle.Event mEvent;
-        private boolean mWasExecuted = false;
-
-        DispatchRunnable(@NonNull LifecycleRegistry registry, Lifecycle.Event event) {
-            mRegistry = registry;
-            mEvent = event;
-        }
-
-        @Override
-        public void run() {
-            if (!mWasExecuted) {
-                mRegistry.handleLifecycleEvent(mEvent);
-                mWasExecuted = true;
-            }
-        }
-    }
-}
diff --git a/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/ServiceLifecycleDispatcher.kt b/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/ServiceLifecycleDispatcher.kt
new file mode 100644
index 0000000..9d2d086
--- /dev/null
+++ b/lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/ServiceLifecycleDispatcher.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.Service
+import android.os.Handler
+
+/**
+ * Helper class to dispatch lifecycle events for a Service. Use it only if it is impossible
+ * to use [LifecycleService].
+ *
+ * @param provider [LifecycleOwner] for a service, usually it is a service itself
+ */
+open class ServiceLifecycleDispatcher(provider: LifecycleOwner) {
+
+    private val registry: LifecycleRegistry
+    private val handler: Handler
+    private var lastDispatchRunnable: DispatchRunnable? = null
+
+    init {
+        registry = LifecycleRegistry(provider)
+        @Suppress("DEPRECATION")
+        handler = Handler()
+    }
+
+    private fun postDispatchRunnable(event: Lifecycle.Event) {
+        lastDispatchRunnable?.run()
+        lastDispatchRunnable = DispatchRunnable(registry, event)
+        handler.postAtFrontOfQueue(lastDispatchRunnable!!)
+    }
+
+    /**
+     * Must be a first call in [Service.onCreate] method, even before super.onCreate call.
+     */
+    open fun onServicePreSuperOnCreate() {
+        postDispatchRunnable(Lifecycle.Event.ON_CREATE)
+    }
+
+    /**
+     * Must be a first call in [Service.onBind] method, even before super.onBind
+     * call.
+     */
+    open fun onServicePreSuperOnBind() {
+        postDispatchRunnable(Lifecycle.Event.ON_START)
+    }
+
+    /**
+     * Must be a first call in [Service.onStart] or
+     * [Service.onStartCommand] methods, even before
+     * a corresponding super call.
+     */
+    open fun onServicePreSuperOnStart() {
+        postDispatchRunnable(Lifecycle.Event.ON_START)
+    }
+
+    /**
+     * Must be a first call in [Service.onDestroy] method, even before super.OnDestroy
+     * call.
+     */
+    open fun onServicePreSuperOnDestroy() {
+        postDispatchRunnable(Lifecycle.Event.ON_STOP)
+        postDispatchRunnable(Lifecycle.Event.ON_DESTROY)
+    }
+
+    /**
+     * [Lifecycle] for the given [LifecycleOwner]
+     */
+    open val lifecycle: Lifecycle
+        get() = registry
+
+    internal class DispatchRunnable(
+        private val registry: LifecycleRegistry,
+        val event: Lifecycle.Event
+    ) : Runnable {
+        private var wasExecuted = false
+
+        override fun run() {
+            if (!wasExecuted) {
+                registry.handleLifecycleEvent(event)
+                wasExecuted = true
+            }
+        }
+    }
+}
\ 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..efdb532 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
@@ -49,7 +49,7 @@
     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 9e83c92..f8457f6 100644
--- a/lifecycle/lifecycle-viewmodel/api/current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/current.txt
@@ -2,13 +2,15 @@
 package androidx.lifecycle {
 
   public class AndroidViewModel extends androidx.lifecycle.ViewModel {
-    ctor public AndroidViewModel(android.app.Application);
+    ctor public AndroidViewModel(android.app.Application application);
     method public <T extends android.app.Application> T getApplication();
   }
 
   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 9e83c92..f8457f6 100644
--- a/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/public_plus_experimental_current.txt
@@ -2,13 +2,15 @@
 package androidx.lifecycle {
 
   public class AndroidViewModel extends androidx.lifecycle.ViewModel {
-    ctor public AndroidViewModel(android.app.Application);
+    ctor public AndroidViewModel(android.app.Application application);
     method public <T extends android.app.Application> T getApplication();
   }
 
   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 9e83c92..f8457f6 100644
--- a/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
+++ b/lifecycle/lifecycle-viewmodel/api/restricted_current.txt
@@ -2,13 +2,15 @@
 package androidx.lifecycle {
 
   public class AndroidViewModel extends androidx.lifecycle.ViewModel {
-    ctor public AndroidViewModel(android.app.Application);
+    ctor public AndroidViewModel(android.app.Application application);
     method public <T extends android.app.Application> T getApplication();
   }
 
   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/AndroidViewModel.java b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/AndroidViewModel.java
deleted file mode 100644
index f3fc032..0000000
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/AndroidViewModel.java
+++ /dev/null
@@ -1,46 +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.annotation.SuppressLint;
-import android.app.Application;
-
-import androidx.annotation.NonNull;
-
-/**
- * Application context aware {@link ViewModel}.
- * <p>
- * Subclasses must have a constructor which accepts {@link Application} as the only parameter.
- * <p>
- */
-public class AndroidViewModel extends ViewModel {
-    @SuppressLint("StaticFieldLeak")
-    private Application mApplication;
-
-    public AndroidViewModel(@NonNull Application application) {
-        mApplication = application;
-    }
-
-    /**
-     * Return the application.
-     */
-    @SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
-    @NonNull
-    public <T extends Application> T getApplication() {
-        return (T) mApplication;
-    }
-}
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/AndroidViewModel.kt b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/AndroidViewModel.kt
new file mode 100644
index 0000000..12f0b18
--- /dev/null
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/AndroidViewModel.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.Application
+
+/**
+ * Application context aware [ViewModel].
+ *
+ * Subclasses must have a constructor which accepts [Application] as the only parameter.
+ */
+open class AndroidViewModel(private val application: Application) : ViewModel() {
+
+    /**
+     * Return the application.
+     */
+    @Suppress("UNCHECKED_CAST")
+    open fun <T : Application> getApplication(): T {
+        return application as T
+    }
+}
\ No newline at end of file
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-player/src/androidTest/java/androidx/media2/player/MediaPlayer2Test.java b/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer2Test.java
index 4f186c0..c12fa6e 100644
--- a/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer2Test.java
+++ b/media2/media2-player/src/androidTest/java/androidx/media2/player/MediaPlayer2Test.java
@@ -1271,6 +1271,7 @@
         return cp;
     }
 
+    @FlakyTest(bugId = 264905014)
     @Test
     @LargeTest
     public void getTimestamp() throws Exception {
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-beta01.txt b/mediarouter/mediarouter-testing/api/1.4.0-beta01.txt
new file mode 100644
index 0000000..14e1df6
--- /dev/null
+++ b/mediarouter/mediarouter-testing/api/1.4.0-beta01.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/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-beta01.txt b/mediarouter/mediarouter-testing/api/public_plus_experimental_1.4.0-beta01.txt
new file mode 100644
index 0000000..14e1df6
--- /dev/null
+++ b/mediarouter/mediarouter-testing/api/public_plus_experimental_1.4.0-beta01.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/mediarouter/mediarouter-testing/api/res-1.4.0-beta01.txt b/mediarouter/mediarouter-testing/api/res-1.4.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/mediarouter/mediarouter-testing/api/res-1.4.0-beta01.txt
diff --git a/mediarouter/mediarouter-testing/api/res-1.4.0-beta02.txt b/mediarouter/mediarouter-testing/api/res-1.4.0-beta02.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/mediarouter/mediarouter-testing/api/res-1.4.0-beta02.txt
diff --git a/mediarouter/mediarouter-testing/api/restricted_1.4.0-beta01.txt b/mediarouter/mediarouter-testing/api/restricted_1.4.0-beta01.txt
new file mode 100644
index 0000000..14e1df6
--- /dev/null
+++ b/mediarouter/mediarouter-testing/api/restricted_1.4.0-beta01.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/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-beta01.txt b/mediarouter/mediarouter/api/1.4.0-beta01.txt
new file mode 100644
index 0000000..b1cf094
--- /dev/null
+++ b/mediarouter/mediarouter/api/1.4.0-beta01.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/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-beta01.txt b/mediarouter/mediarouter/api/public_plus_experimental_1.4.0-beta01.txt
new file mode 100644
index 0000000..b1cf094
--- /dev/null
+++ b/mediarouter/mediarouter/api/public_plus_experimental_1.4.0-beta01.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-beta01.txt b/mediarouter/mediarouter/api/res-1.4.0-beta01.txt
new file mode 100644
index 0000000..620c3fe
--- /dev/null
+++ b/mediarouter/mediarouter/api/res-1.4.0-beta01.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/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-beta01.txt b/mediarouter/mediarouter/api/restricted_1.4.0-beta01.txt
new file mode 100644
index 0000000..b1cf094
--- /dev/null
+++ b/mediarouter/mediarouter/api/restricted_1.4.0-beta01.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/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 1446b74..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
      */
@@ -150,7 +158,6 @@
         Intent intent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS)
                 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK)
                 .putExtra("EXTRA_CONNECTION_ONLY", true)
-                .putExtra("EXTRA_CLOSE_ON_CONNECT", true)
                 .putExtra("android.bluetooth.devicepicker.extra.FILTER_TYPE", 1);
 
         PackageManager packageManager = context.getPackageManager();
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/mediarouter/mediarouter/src/test/java/androidx/mediarouter/app/SystemOutputSwitcherDialogControllerTest.java b/mediarouter/mediarouter/src/test/java/androidx/mediarouter/app/SystemOutputSwitcherDialogControllerTest.java
index b3314fa..24b8546 100644
--- a/mediarouter/mediarouter/src/test/java/androidx/mediarouter/app/SystemOutputSwitcherDialogControllerTest.java
+++ b/mediarouter/mediarouter/src/test/java/androidx/mediarouter/app/SystemOutputSwitcherDialogControllerTest.java
@@ -105,7 +105,7 @@
         assertThat(intent.getAction())
                 .isEqualTo(Settings.ACTION_BLUETOOTH_SETTINGS);
         assertThat(intent.getBooleanExtra("EXTRA_CONNECTION_ONLY", false)).isTrue();
-        assertThat(intent.getBooleanExtra("EXTRA_CLOSE_ON_CONNECT", false)).isTrue();
+        assertThat(intent.getBooleanExtra("EXTRA_CLOSE_ON_CONNECT", false)).isFalse();
         assertThat(intent.getIntExtra("android.bluetooth.devicepicker.extra.FILTER_TYPE", 0))
                 .isEqualTo(1);
     }
diff --git a/navigation/navigation-common/api/current.txt b/navigation/navigation-common/api/current.txt
index 2133a87..a0905df 100644
--- a/navigation/navigation-common/api/current.txt
+++ b/navigation/navigation-common/api/current.txt
@@ -123,10 +123,13 @@
     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 final androidx.lifecycle.SavedStateHandle savedStateHandle;
     property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
+    property public androidx.lifecycle.ViewModelStore viewModelStore;
   }
 
   public final class NavDeepLink {
@@ -505,6 +508,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..a0905df 100644
--- a/navigation/navigation-common/api/public_plus_experimental_current.txt
+++ b/navigation/navigation-common/api/public_plus_experimental_current.txt
@@ -123,10 +123,13 @@
     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 final androidx.lifecycle.SavedStateHandle savedStateHandle;
     property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
+    property public androidx.lifecycle.ViewModelStore viewModelStore;
   }
 
   public final class NavDeepLink {
@@ -505,6 +508,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..a0905df 100644
--- a/navigation/navigation-common/api/restricted_current.txt
+++ b/navigation/navigation-common/api/restricted_current.txt
@@ -123,10 +123,13 @@
     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 final androidx.lifecycle.SavedStateHandle savedStateHandle;
     property public androidx.savedstate.SavedStateRegistry savedStateRegistry;
+    property public androidx.lifecycle.ViewModelStore viewModelStore;
   }
 
   public final class NavDeepLink {
@@ -505,6 +508,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/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..953aae9 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavBackStackEntry.kt
@@ -197,46 +197,46 @@
         }
     }
 
-    /**
-     * {@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
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..1137efa 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -28,7 +28,7 @@
 
     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")
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..6314eaf 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
@@ -790,7 +790,7 @@
         val onBackPressedDispatcher = OnBackPressedDispatcher()
         val dispatcherOwner = object : OnBackPressedDispatcherOwner {
             override fun getLifecycle() = lifecycleOwner.lifecycle
-            override fun getOnBackPressedDispatcher() = onBackPressedDispatcher
+            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/NavControllerWithFragmentTest.kt b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
index 8103e71..66de182 100644
--- a/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
+++ b/navigation/navigation-fragment/src/androidTest/java/androidx/navigation/fragment/NavControllerWithFragmentTest.kt
@@ -16,6 +16,9 @@
 
 package androidx.navigation.fragment
 
+import android.app.Dialog
+import androidx.fragment.app.DialogFragment
+import android.os.Bundle
 import androidx.navigation.NavOptions
 import androidx.navigation.fragment.test.NavigationActivity
 import androidx.test.core.app.ActivityScenario
@@ -25,6 +28,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import androidx.navigation.fragment.test.R
+import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 
 @LargeTest
@@ -32,30 +36,203 @@
 class NavControllerWithFragmentTest {
 
     @Test
-    fun navigateWithSingleTop() {
+    fun fragmentNavigateWithSingleTop() = withNavigationActivity {
+        navController.navigate(R.id.empty_fragment)
+
+        val fm =
+            supportFragmentManager.findFragmentById(R.id.nav_host)?.childFragmentManager
+        fm?.executePendingTransactions()
+        val fragment = fm?.findFragmentById(R.id.nav_host)
+
+        navController.navigate(
+            R.id.empty_fragment,
+            null,
+            NavOptions.Builder().setLaunchSingleTop(true).build()
+        )
+
+        fm?.executePendingTransactions()
+
+        val replacementFragment = fm?.findFragmentById(R.id.nav_host)
+
+        assertWithMessage("Replacement should be a new instance")
+            .that(replacementFragment)
+            .isNotSameInstanceAs(fragment)
+    }
+
+    @Test
+    fun dialogFragmentNavigate_singleTop() = withNavigationActivity {
+        val navigator =
+            navController.navigatorProvider.getNavigator(DialogFragmentNavigator::class.java)
+        navController.navigate(R.id.testDialog_fragment)
+
+        val fm = supportFragmentManager.findFragmentById(R.id.nav_host)?.childFragmentManager
+        fm?.executePendingTransactions()
+
+        assertThat(navController.currentBackStackEntry?.destination?.id)
+            .isEqualTo(R.id.testDialog_fragment)
+        val originalFragment = fm?.findFragmentByTag(navController.currentBackStackEntry?.id)
+            as? TestDialogFragment
+        assertThat(originalFragment!!.dialogs.first().isShowing).isTrue()
+
+        // backStacks should be in sync
+        assertThat(navigator.backStack.value.size).isEqualTo(1)
+        assertThat(fm.fragments.size).isEqualTo(2) // start + dialog fragment
+
+        // singleTop navigation
+        navController.navigate(
+            R.id.testDialog_fragment,
+            null,
+            NavOptions.Builder().setLaunchSingleTop(true).build()
+        )
+        assertThat(navController.currentBackStackEntry?.destination?.id)
+            .isEqualTo(R.id.testDialog_fragment)
+        fm.executePendingTransactions()
+
+        assertThat(navController.currentBackStackEntry?.destination?.id)
+            .isEqualTo(R.id.testDialog_fragment)
+        val replacementFragment = fm.findFragmentByTag(navController.currentBackStackEntry?.id)
+            as? TestDialogFragment
+        // the first dialog should be dismissed
+        assertThat(originalFragment.dialogs.first().isShowing).isFalse()
+        assertThat(replacementFragment!!.dialogs.first().isShowing).isTrue()
+
+        assertWithMessage("Replacement should be a new instance")
+            .that(originalFragment)
+            .isNotSameInstanceAs(replacementFragment)
+
+        // backStacks should be in sync
+        assertThat(navigator.backStack.value.size).isEqualTo(1)
+        assertThat(fm.fragments.size).isEqualTo(2) // start + dialog fragment
+    }
+
+    @Test
+    fun dialogFragmentNavigate_immediateSingleTop() = withNavigationActivity {
+        val navigator =
+            navController.navigatorProvider.getNavigator(DialogFragmentNavigator::class.java)
+
+        // first navigation
+        navController.navigate(R.id.testDialog_fragment)
+
+        // immediate second navigation with singleTop without executing first transaction
+        navController.navigate(
+            R.id.testDialog_fragment,
+            null,
+            NavOptions.Builder().setLaunchSingleTop(true).build()
+        )
+        assertThat(navController.currentBackStackEntry?.destination?.id)
+            .isEqualTo(R.id.testDialog_fragment)
+
+        val fm = supportFragmentManager.findFragmentById(R.id.nav_host)?.childFragmentManager
+        fm?.executePendingTransactions()
+
+        assertThat(navController.currentBackStackEntry?.destination?.id)
+            .isEqualTo(R.id.testDialog_fragment)
+        val replacementFragment = fm?.findFragmentByTag(navController.currentBackStackEntry?.id)
+            as? TestDialogFragment
+
+        assertThat(replacementFragment!!.dialogs.first().isShowing).isTrue()
+
+        // ensure original Fragment is dismissed and backStacks are in sync
+        assertThat(navigator.backStack.value.size).isEqualTo(1)
+        assertThat(fm.fragments.size).isEqualTo(2) // start + dialog fragment
+    }
+
+    @Test
+    fun dialogFragmentNavigate_multiImmediateSingleTop() = withNavigationActivity {
+        val navigator =
+            navController.navigatorProvider.getNavigator(DialogFragmentNavigator::class.java)
+
+        // first navigation
+        navController.navigate(R.id.testDialog_fragment)
+
+        // immediate second navigation with singleTop without executing first transaction
+        navController.navigate(
+            R.id.testDialog_fragment,
+            null,
+            NavOptions.Builder().setLaunchSingleTop(true).build()
+        )
+
+        // immediate third navigation with singleTop without executing previous transactions
+        navController.navigate(
+            R.id.testDialog_fragment,
+            null,
+            NavOptions.Builder().setLaunchSingleTop(true).build()
+        )
+
+        assertThat(navController.currentBackStackEntry?.destination?.id)
+            .isEqualTo(R.id.testDialog_fragment)
+
+        val fm = supportFragmentManager.findFragmentById(R.id.nav_host)?.childFragmentManager
+        fm?.executePendingTransactions()
+
+        assertThat(navController.currentBackStackEntry?.destination?.id)
+            .isEqualTo(R.id.testDialog_fragment)
+        val replacementFragment = fm?.findFragmentByTag(navController.currentBackStackEntry?.id)
+            as? TestDialogFragment
+
+        assertThat(replacementFragment!!.dialogs.first().isShowing).isTrue()
+
+        // ensure previous fragments are dismissed and backStacks are in sync
+        assertThat(navigator.backStack.value.size).isEqualTo(1)
+        assertThat(fm.fragments.size).isEqualTo(2) // start + dialog fragment
+    }
+
+    @Test
+    fun dialogFragmentNavigate_partiallyImmediateSingleTop() = withNavigationActivity {
+        val navigator =
+            navController.navigatorProvider.getNavigator(DialogFragmentNavigator::class.java)
+
+        // first navigation
+        navController.navigate(R.id.testDialog_fragment)
+
+        val fm = supportFragmentManager.findFragmentById(R.id.nav_host)?.childFragmentManager
+        fm?.executePendingTransactions()
+
+        // second navigation with singleTop
+        navController.navigate(
+            R.id.testDialog_fragment,
+            null,
+            NavOptions.Builder().setLaunchSingleTop(true).build()
+        )
+
+        // immediate third navigation with singleTop without executing previous singleTop
+        navController.navigate(
+            R.id.testDialog_fragment,
+            null,
+            NavOptions.Builder().setLaunchSingleTop(true).build()
+        )
+
+        fm?.executePendingTransactions()
+
+        assertThat(navController.currentBackStackEntry?.destination?.id)
+            .isEqualTo(R.id.testDialog_fragment)
+        val replacementFragment = fm?.findFragmentByTag(navController.currentBackStackEntry?.id)
+            as? TestDialogFragment
+
+        assertThat(replacementFragment!!.dialogs.first().isShowing).isTrue()
+
+        // ensure previous fragments are dismissed and backStacks are in sync
+        assertThat(navigator.backStack.value.size).isEqualTo(1)
+        assertThat(fm.fragments.size).isEqualTo(2) // start + last dialog fragment
+    }
+
+    private fun withNavigationActivity(
+        block: NavigationActivity.() -> Unit
+    ) {
         with(ActivityScenario.launch(NavigationActivity::class.java)) {
             withActivity {
-                navController.navigate(R.id.empty_fragment)
-
-                val fm =
-                    supportFragmentManager.findFragmentById(R.id.nav_host)?.childFragmentManager
-                fm?.executePendingTransactions()
-                val fragment = fm?.findFragmentById(R.id.nav_host)
-
-                navController.navigate(
-                    R.id.empty_fragment,
-                    null,
-                    NavOptions.Builder().setLaunchSingleTop(true).build()
-                )
-
-                fm?.executePendingTransactions()
-
-                val replacementFragment = fm?.findFragmentById(R.id.nav_host)
-
-                assertWithMessage("Replacement should be a new instance")
-                    .that(replacementFragment)
-                    .isNotSameInstanceAs(fragment)
+                this.block()
             }
         }
     }
+}
+
+class TestDialogFragment : DialogFragment() {
+    val dialogs = mutableListOf<Dialog>()
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        val dialog = super.onCreateDialog(savedInstanceState)
+        dialogs.add(dialog)
+        return dialog
+    }
 }
\ No newline at end of file
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/androidTest/res/navigation/nav_simple.xml b/navigation/navigation-fragment/src/androidTest/res/navigation/nav_simple.xml
index 82681b8..a732a62 100644
--- a/navigation/navigation-fragment/src/androidTest/res/navigation/nav_simple.xml
+++ b/navigation/navigation-fragment/src/androidTest/res/navigation/nav_simple.xml
@@ -31,4 +31,7 @@
     <dialog
         android:id="@+id/dialog_fragment"
         android:name="androidx.navigation.fragment.EmptyDialogFragment"/>
+    <dialog
+        android:id="@+id/testDialog_fragment"
+        android:name="androidx.navigation.fragment.TestDialogFragment"/>
 </navigation>
\ No newline at end of file
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 8ab8cc8..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 */ }
             }
         }
     }
@@ -83,6 +107,11 @@
     internal val backStack: StateFlow<List<NavBackStackEntry>>
         get() = state.backStack
 
+    /**
+     * Stores DialogFragments that have been created but pendingTransaction.
+     */
+    private val transitioningFragments: MutableMap<String, DialogFragment> = mutableMapOf()
+
     override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
         if (fragmentManager.isStateSaved) {
             Log.i(
@@ -101,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 {
@@ -129,6 +157,35 @@
     private fun navigate(
         entry: NavBackStackEntry
     ) {
+        val dialogFragment = createDialogFragment(entry)
+        dialogFragment.show(fragmentManager, entry.id)
+        state.pushWithTransition(entry)
+    }
+
+    override fun onLaunchSingleTop(backStackEntry: NavBackStackEntry) {
+        if (fragmentManager.isStateSaved) {
+            Log.i(
+                TAG,
+                "Ignoring onLaunchSingleTop() call: FragmentManager has already saved its state"
+            )
+            return
+        }
+
+        // Ensure previous fragment is dismissed. If it is in transition, we have to dismiss it
+        // here before its value with same key (entry.id) gets replaced by new fragment.
+        val oldFragment = transitioningFragments[backStackEntry.id]
+            ?: fragmentManager.findFragmentByTag(backStackEntry.id) as? DialogFragment
+        if (oldFragment != null) {
+            oldFragment.lifecycle.removeObserver(observer)
+            oldFragment.dismiss()
+        }
+
+        val newFragment = createDialogFragment(backStackEntry)
+        newFragment.show(fragmentManager, backStackEntry.id)
+        state.onLaunchSingleTopWithTransition(backStackEntry)
+    }
+
+    private fun createDialogFragment(entry: NavBackStackEntry): DialogFragment {
         val destination = entry.destination as Destination
         var className = destination.className
         if (className[0] == '.') {
@@ -143,8 +200,12 @@
         val dialogFragment = frag as DialogFragment
         dialogFragment.arguments = entry.arguments
         dialogFragment.lifecycle.addObserver(observer)
-        dialogFragment.show(fragmentManager, entry.id)
-        state.push(entry)
+        // For singleTop navigations, this will overwrite existing transitioning fragments with
+        // the same `entry.id`. This is fine because before the singleTop DialogFragment
+        // is recreated and replaces the old record inside transitioningFragments, we would
+        // have already dismissed the existing (old) fragment.
+        transitioningFragments[entry.id] = dialogFragment
+        return dialogFragment
     }
 
     override fun onAttach(state: NavigatorState) {
@@ -160,6 +221,7 @@
             if (needToAddObserver) {
                 childFragment.lifecycle.addObserver(observer)
             }
+            transitioningFragments.remove(childFragment.tag)
         }
     }
 
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 2c7f44a..6f25f9e 100644
--- a/paging/paging-testing/api/current.txt
+++ b/paging/paging-testing/api/current.txt
@@ -6,8 +6,11 @@
   }
 
   public final class SnapshotLoader<Value> {
-    method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function2<Value,? super kotlin.coroutines.Continuation<java.lang.Boolean>,?> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
+    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 2c7f44a..6f25f9e 100644
--- a/paging/paging-testing/api/public_plus_experimental_current.txt
+++ b/paging/paging-testing/api/public_plus_experimental_current.txt
@@ -6,8 +6,11 @@
   }
 
   public final class SnapshotLoader<Value> {
-    method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function2<Value,? super kotlin.coroutines.Continuation<java.lang.Boolean>,?> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
+    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 2c7f44a..6f25f9e 100644
--- a/paging/paging-testing/api/restricted_current.txt
+++ b/paging/paging-testing/api/restricted_current.txt
@@ -6,8 +6,11 @@
   }
 
   public final class SnapshotLoader<Value> {
-    method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function2<Value,? super kotlin.coroutines.Continuation<java.lang.Boolean>,?> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
+    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 381cf97..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
@@ -24,11 +25,16 @@
 import androidx.paging.Pager
 import androidx.paging.PagingData
 import androidx.paging.PagingDataDiffer
+import androidx.paging.testing.LoaderCallback.CallbackType.ON_CHANGED
+import androidx.paging.testing.LoaderCallback.CallbackType.ON_INSERTED
+import androidx.paging.testing.LoaderCallback.CallbackType.ON_REMOVED
 import kotlinx.coroutines.CoroutineScope
 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
@@ -51,13 +57,22 @@
 
     val callback = object : DifferCallback {
         override fun onChanged(position: Int, count: Int) {
-            loader.onDataSetChanged(loader.generations.value)
+            loader.onDataSetChanged(
+                loader.generations.value,
+                LoaderCallback(ON_CHANGED, position, count)
+            )
         }
         override fun onInserted(position: Int, count: Int) {
-            loader.onDataSetChanged(loader.generations.value)
+            loader.onDataSetChanged(
+                loader.generations.value,
+                LoaderCallback(ON_INSERTED, position, count)
+            )
         }
         override fun onRemoved(position: Int, count: Int) {
-            loader.onDataSetChanged(loader.generations.value)
+            loader.onDataSetChanged(
+                loader.generations.value,
+                LoaderCallback(ON_REMOVED, position, count)
+            )
         }
     }
 
@@ -96,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)
@@ -117,7 +132,7 @@
         loader.loadOperations()
         differ.awaitNotLoading()
 
-        job.cancelAndJoin()
+        collectPagingData.cancelAndJoin()
 
         differ.snapshot().items
     }
@@ -127,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 86c1b61..6fdc7a3 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,16 +17,20 @@
 package androidx.paging.testing
 
 import androidx.paging.DifferCallback
+import androidx.paging.LoadType.APPEND
+import androidx.paging.LoadType.PREPEND
+import androidx.paging.PagingConfig
 import androidx.paging.PagingData
 import androidx.paging.PagingDataDiffer
 import androidx.paging.PagingSource
-import androidx.paging.LoadType.APPEND
-import androidx.paging.PagingConfig
+import androidx.paging.testing.LoaderCallback.CallbackType.ON_INSERTED
 import java.util.concurrent.atomic.AtomicInteger
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.math.abs
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.map
 
 /**
  * Contains the public APIs for load operations in tests.
@@ -73,70 +77,326 @@
      * @param [predicate] the predicate to match (return true) to continue append scrolls
      */
     public suspend fun appendScrollWhile(
-        predicate: suspend (item: @JvmSuppressWildcards Value) -> @JvmSuppressWildcards Boolean
+        predicate: (item: @JvmSuppressWildcards Value) -> @JvmSuppressWildcards Boolean
     ): @JvmSuppressWildcards Unit {
         differ.awaitNotLoading()
-        appendOrPrepend(LoadType.APPEND, predicate)
+        appendOrPrependScrollWhile(LoadType.APPEND, predicate)
         differ.awaitNotLoading()
     }
 
-    private suspend fun appendOrPrepend(
+    /**
+     * Imitates scrolling up paged items, [prepending][PREPEND] data until the given
+     * predicate returns false.
+     *
+     * Note: This API loads an item before passing it into the predicate. This means the
+     * loaded pages may include the page which contains the item that does not match the
+     * predicate. For example, if pageSize = 2, initialKey = 3, the predicate
+     * {item: Int -> item >= 3 } will return items [[1, 2],[3, 4]] where [1, 2] is the page
+     * containing the boundary item[2] not matching the predicate.
+     *
+     * The loaded pages are also dependent on [PagingConfig] settings such as
+     * [PagingConfig.prefetchDistance]:
+     * - if `prefetchDistance` > 0, the resulting prepends will include prefetched items.
+     * For example, if pageSize = 2, initialKey = 3, and prefetchDistance = 2, the predicate
+     * {item: Int -> item > 4 } will load items [[1, 2], [3, 4], [5, 6]] where both [1,2] and
+     * [5, 6] are the prefetched pages.
+     *
+     * @param [predicate] the predicate to match (return true) to continue prepend scrolls
+     */
+    public suspend fun prependScrollWhile(
+        predicate: (item: @JvmSuppressWildcards Value) -> @JvmSuppressWildcards Boolean
+    ): @JvmSuppressWildcards Unit {
+        differ.awaitNotLoading()
+        appendOrPrependScrollWhile(LoadType.PREPEND, predicate)
+        differ.awaitNotLoading()
+    }
+
+    private suspend fun appendOrPrependScrollWhile(
         loadType: LoadType,
-        predicate: suspend (item: Value) -> Boolean
+        predicate: (item: Value) -> Boolean
     ) {
         do {
-            // Get and update the index to load from. Return if index is invalid.
-            val index = nextLoadIndexOrNull(loadType) ?: return
-            val item = loadItem(index)
+            // awaits for next item where the item index is determined based on
+            // this generation's lastAccessedIndex. If null, it means there are no more
+            // items to load for this loadType.
+            val item = awaitNextItem(loadType) ?: return
         } while (predicate(item))
     }
 
     /**
+     * Imitates scrolling from current index to the target index. It waits for an item to be loaded
+     * in before triggering load on next item. Returns all available data that has been scrolled
+     * through.
+     *
+     * The scroll direction (prepend or append) is dependent on current index and target index. In
+     * general, scrolling to a smaller index triggers [PREPEND] while scrolling to a larger
+     * index triggers [APPEND].
+     *
+     * When [PagingConfig.enablePlaceholders] is false, the [index] is scoped within currently
+     * loaded items. For example, in a list of items(0-20) with currently loaded items(10-15),
+     * index[0] = item(10), index[4] = item(15).
+     *
+     * Supports [index] beyond currently loaded items when [PagingConfig.enablePlaceholders]
+     * is false:
+     * 1. For prepends, it supports negative indices for as long as there are still available
+     * data to load from. For example, take a list of items(0-20), pageSize = 1, with currently
+     * loaded items(10-15). With index[0] = item(10), a `scrollTo(-4)` will scroll to item(6) and
+     * update index[0] = item(6).
+     * 2. For appends, it supports indices >= loadedDataSize. For example, take a list of
+     * items(0-20), pageSize = 1, with currently loaded items(10-15). With
+     * index[4] = item(15), a `scrollTo(7)` will scroll to item(18) and update
+     * index[7] = item(18).
+     * Note that both examples does not account for prefetches.
+
+     * The [index] accounts for separators/headers/footers where each one of those consumes one
+     * scrolled index.
+     *
+     * For both append/prepend, this function stops loading prior to fulfilling requested scroll
+     * distance if there are no more data to load from.
+     *
+     * @param [index] The target index to scroll to
+     *
+     * @see [flingTo] for faking a scroll that continues scrolling without waiting for items to
+     * be loaded in. Supports jumping.
+     */
+    public suspend fun scrollTo(index: Int): @JvmSuppressWildcards Unit {
+        differ.awaitNotLoading()
+        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 -> {
+                // TODO
+            }
+        }
+    }
+
+    /**
+     * Prepend flings to target index.
+     *
+     * If target index is negative, from index[0] onwards it will normal scroll until it fulfills
+     * requested scroll 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 required to
+        // the awaiting version.
+        if (index < 0) {
+            flingToOutOfBounds(LoadType.PREPEND, lastAccessedIndex, index)
+        }
+    }
+
+    /**
+     * Delegated work from [flingTo] that is responsible for scrolling to indices that is
+     * beyond [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,
+        index: 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
+        // below zero which is abs(index)
+        awaitScroll(loadType, abs(index))
+    }
+
+    private suspend fun awaitScroll(loadType: LoadType, scrollCount: Int) {
+        repeat(scrollCount) {
+            awaitNextItem(loadType) ?: return
+        }
+    }
+
+    /**
+     * Triggers load for next item, awaits for it to be loaded and returns the loaded item.
+     *
+     * It calculates the next load index based on loadType and this generation's
+     * [Generation.lastAccessedIndex]. The lastAccessedIndex is updated when item is loaded in.
+     */
+    private suspend fun awaitNextItem(loadType: LoadType): Value? {
+        // Get the index to load from. Return if index is invalid.
+        val index = nextIndexOrNull(loadType) ?: return null
+        // OffsetIndex accounts for items that are prepended when placeholders are disabled,
+        // as the new items shift the position of existing items. The offsetIndex (which may
+        // or may not be the same as original index) is stored as lastAccessedIndex after load and
+        // becomes the basis for next load index.
+        val (item, offsetIndex) = awaitLoad(index)
+        setLastAccessedIndex(offsetIndex)
+        return item
+    }
+
+    /**
      * Get and update the index to load from. Returns null if next index is out of bounds.
      *
-     * This method is responsible for updating the [Generation.lastAccessedIndex] that is then sent
-     * to the differ to trigger load for that index.
+     * This method computes the next load index based on the [LoadType] and
+     * [Generation.lastAccessedIndex]
      */
-    private fun nextLoadIndexOrNull(loadType: LoadType): Int? {
-        val currGen = generations.value
+    private fun nextIndexOrNull(loadType: LoadType): Int? {
+        val currIndex = generations.value.lastAccessedIndex.get()
         return when (loadType) {
             LoadType.PREPEND -> {
-                if (currGen.lastAccessedIndex.get() <= 0) {
+                if (currIndex <= 0) {
                     return null
                 }
-                currGen.lastAccessedIndex.decrementAndGet()
+                currIndex - 1
             }
             LoadType.APPEND -> {
-                if (currGen.lastAccessedIndex.get() >= differ.size - 1) {
+                if (currIndex >= differ.size - 1) {
                     return null
                 }
-                currGen.lastAccessedIndex.incrementAndGet()
+                currIndex + 1
             }
         }
     }
 
     // Executes actual loading by accessing the PagingDataDiffer
     @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
-    private suspend fun loadItem(index: Int): Value {
+    private suspend fun awaitLoad(index: Int): Pair<Value, Int> {
         differ[index]
+        differ.awaitNotLoading()
+        var offsetIndex = index
 
         // awaits for the item to be loaded
-        return generations.mapLatest {
-            differ.peek(index)
-        }.filterNotNull().first()
+        return generations.map { generation ->
+            // reset callbackState to null so it doesn't get applied on the next load
+            val callbackState = generation.callbackState.getAndSet(null)
+            // offsetIndex accounts for items prepended when placeholders are disabled. This
+            // is necessary because the new items shift the position of existing items, and
+            // the index no longer tracks the correct item.
+            offsetIndex += callbackState?.computeIndexOffset() ?: 0
+            differ.peek(offsetIndex)
+        }.filterNotNull().first() to offsetIndex
+    }
+
+    /**
+     * Computes the offset to add to the index when loading items from differ.
+     *
+     * The purpose of this is to address shifted item positions when new items are prepended
+     * with placeholders disabled. For example, loaded items(10-12) in the NullPaddedList
+     * would have item(12) at differ[2]. If we prefetched items(7-9), item(12) would now be in
+     * differ[5].
+     *
+     * Without the offset, [PREPEND] operations would call differ[1] to load next item(11)
+     * which would now yield item(8) instead of item(11). The offset would add the
+     * inserted count to the next load index such that after prepending 3 new items(7-9), the next
+     * [PREPEND] operation would call differ[1+3 = 4] to properly load next item(11).
+     *
+     * This method is essentially no-op unless the callback meets three conditions:
+     * - is type [DifferCallback.onChanged]
+     * - position is 0 as we only care about item prepended to front of list
+     * - inserted count > 0
+     */
+    private fun LoaderCallback.computeIndexOffset(): Int {
+        return if (type == ON_INSERTED && position == 0) count else 0
+    }
+
+    private fun setLastAccessedIndex(index: Int) {
+        generations.value.lastAccessedIndex.set(index)
     }
 
     /**
      * The callback to be invoked by DifferCallback on a single generation.
      * Increase the callbackCount to notify SnapshotLoader that the dataset has updated
      */
-    internal fun onDataSetChanged(gen: Generation) {
+    internal fun onDataSetChanged(gen: Generation, callback: LoaderCallback) {
         val currGen = generations.value
         // we make sure the generation with the dataset change is still valid because we
         // want to disregard callbacks on stale generations
         if (gen.id == currGen.id) {
             generations.value = gen.copy(
-                callbackCount = currGen.callbackCount + 1
+                callbackCount = currGen.callbackCount + 1,
+                callbackState = currGen.callbackState.apply { set(callback) }
             )
         }
     }
@@ -164,7 +424,25 @@
     val callbackCount: Int = 0,
 
     /**
-     * Tracks the last accessed index on the differ for this generation
+     * Temporarily stores the latest [DifferCallback] to track prepends to the beginning of list.
+     * Value is reset to null once read.
+     */
+    val callbackState: AtomicReference<LoaderCallback?> = AtomicReference(null),
+
+    /**
+     * Tracks the last accessed(peeked) index on the differ for this generation
       */
     var lastAccessedIndex: AtomicInteger = AtomicInteger()
-)
\ No newline at end of file
+)
+
+internal data class LoaderCallback(
+    val type: CallbackType,
+    val position: Int,
+    val count: Int,
+) {
+    internal enum class CallbackType {
+        ON_CHANGED,
+        ON_INSERTED,
+        ON_REMOVED,
+    }
+}
\ No newline at end of file
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 5b4fd14..ba5f827 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,11 +68,9 @@
     @Test
     fun initialRefresh() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
-
         val pager = Pager(
             config = CONFIG,
-            pagingSourceFactory = factory
+            pagingSourceFactory = createFactory(dataFlow)
         )
         testScope.runTest {
             val snapshot = pager.flow.asSnapshot(this) {}
@@ -65,12 +82,31 @@
     }
 
     @Test
+    fun initialRefresh_withSeparators() {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = Pager(
+            config = CONFIG,
+            pagingSourceFactory = createFactory(dataFlow)
+        ).flow.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
+            pagingSourceFactory = createFactory(dataFlow)
         )
         testScope.runTest {
             val snapshot = pager.flow.asSnapshot(this) {}
@@ -84,11 +120,10 @@
     @Test
     fun initialRefresh_withInitialKey() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
         val pager = Pager(
             config = CONFIG,
             initialKey = 10,
-            pagingSourceFactory = factory
+            pagingSourceFactory = createFactory(dataFlow)
         )
         testScope.runTest {
             val snapshot = pager.flow.asSnapshot(this) {}
@@ -102,11 +137,10 @@
     @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
+            pagingSourceFactory = createFactory(dataFlow)
         )
         testScope.runTest {
             val snapshot = pager.flow.asSnapshot(this) {}
@@ -120,10 +154,9 @@
     @Test
     fun emptyInitialRefresh() {
         val dataFlow = emptyFlow<List<Int>>()
-        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
         val pager = Pager(
             config = CONFIG,
-            pagingSourceFactory = factory
+            pagingSourceFactory = createFactory(dataFlow)
         )
         testScope.runTest {
             val snapshot = pager.flow.asSnapshot(this) {}
@@ -137,10 +170,9 @@
     @Test
     fun manualRefresh() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
         val pager = Pager(
             config = CONFIG_NO_PREFETCH,
-            pagingSourceFactory = factory
+            pagingSourceFactory = createFactory(dataFlow)
         ).flow.cachedIn(testScope.backgroundScope)
 
         testScope.runTest {
@@ -156,10 +188,9 @@
     @Test
     fun manualEmptyRefresh() {
         val dataFlow = emptyFlow<List<Int>>()
-        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
         val pager = Pager(
             config = CONFIG_NO_PREFETCH,
-            pagingSourceFactory = factory
+            pagingSourceFactory = createFactory(dataFlow)
         )
         testScope.runTest {
             val snapshot = pager.flow.asSnapshot(this) {
@@ -172,12 +203,11 @@
     }
 
     @Test
-    fun append() {
+    fun appendWhile() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
         val pager = Pager(
             config = CONFIG,
-            pagingSourceFactory = factory,
+            pagingSourceFactory = createFactory(dataFlow),
         )
         testScope.runTest {
             val snapshot = pager.flow.asSnapshot(this) {
@@ -194,13 +224,84 @@
             )
         }
     }
+
     @Test
-    fun append_withoutPlaceholders() {
+    fun appendWhile_withDrops() {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = Pager(
+            config = CONFIG_WITH_DROPS,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow
+        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 = Pager(
+            config = CONFIG,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 = createFactory(dataFlow),
+        ).flow
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                appendScrollWhile { item: Int ->
+                    item < 14
+                }
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                // initial load [0-4]
+                // appended [5-16]
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+            )
+        }
+    }
+
+    @Test
+    fun appendWhile_withoutPlaceholders() {
+        val dataFlow = flowOf(List(50) { it })
         val pager = Pager(
             config = CONFIG_NO_PLACEHOLDERS,
-            pagingSourceFactory = factory,
+            pagingSourceFactory = createFactory(dataFlow),
         ).flow
         testScope.runTest {
             val snapshot = pager.asSnapshot(this) {
@@ -219,35 +320,136 @@
     }
 
     @Test
-    fun append_withoutPrefetch() {
-        val dataFlow = flowOf(List(50) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
+    fun prependWhile() {
+        val dataFlow = flowOf(List(30) { it })
         val pager = Pager(
-            config = CONFIG_NO_PREFETCH,
-            pagingSourceFactory = factory,
-        ).flow
+            config = CONFIG,
+            initialKey = 20,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
         testScope.runTest {
-            val snapshot = pager.asSnapshot(this) {
-                appendScrollWhile { item: Int ->
-                    item < 14
+            val snapshot = pager.flow.asSnapshot(this) {
+                prependScrollWhile { item: Int ->
+                    item > 14
                 }
             }
+            // initial load [20-24]
+            // prefetched [17-19], [25-27]
+            // prepended [14-16]
+            // prefetched [11-13]
             assertThat(snapshot).containsExactlyElementsIn(
-                // initial load [0-4]
-                // appended [5-16]
-                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
+                listOf(11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27)
             )
         }
     }
 
     @Test
-    fun append_withInitialKey() {
+    fun prependWhile_withDrops() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
+        val pager = Pager(
+            config = CONFIG_WITH_DROPS,
+            initialKey = 20,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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 = Pager(
+            config = CONFIG,
+            initialKey = 20,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 pager = Pager(
+            config = CONFIG_NO_PREFETCH,
+            initialKey = 20,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                prependScrollWhile { item: Int ->
+                    item > 14
+                }
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                // initial load [20-24]
+                // prepended [14-19]
+                listOf(14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24)
+            )
+        }
+    }
+
+    @Test
+    fun prependWhile_withoutPlaceholders() {
+        val dataFlow = flowOf(List(50) { it })
+        val pager = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            initialKey = 30,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                prependScrollWhile { item: Int ->
+                    item != 22
+                }
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                // initial load [30-34]
+                // prefetched [27-29], [35-37]
+                // prepended [21-26]
+                // prefetched [18-20]
+                listOf(
+                    18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37
+                )
+            )
+        }
+    }
+
+    @Test
+    fun appendWhile_withInitialKey() {
+        val dataFlow = flowOf(List(30) { it })
         val pager = Pager(
             config = CONFIG,
             initialKey = 10,
-            pagingSourceFactory = factory,
+            pagingSourceFactory = createFactory(dataFlow),
         )
         testScope.runTest {
             val snapshot = pager.flow.asSnapshot(this) {
@@ -266,13 +468,12 @@
     }
 
     @Test
-    fun append_withInitialKey_withoutPlaceholders() {
+    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,
+            pagingSourceFactory = createFactory(dataFlow),
         )
         testScope.runTest {
             val snapshot = pager.flow.asSnapshot(this) {
@@ -291,13 +492,12 @@
     }
 
     @Test
-    fun append_withInitialKey_withoutPrefetch() {
+    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,
+            pagingSourceFactory = createFactory(dataFlow),
         )
         testScope.runTest {
             val snapshot = pager.flow.asSnapshot(this) {
@@ -314,12 +514,32 @@
     }
 
     @Test
-    fun consecutiveAppend() {
+    fun prependWhile_withoutInitialKey() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
         val pager = Pager(
             config = CONFIG,
-            pagingSourceFactory = factory,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot(this) {
+                prependScrollWhile { item: Int ->
+                    item > -3
+                }
+            }
+            // initial load [0-4]
+            // prefetched [5-7]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4, 5, 6, 7)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveAppendWhile() {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = Pager(
+            config = CONFIG,
+            pagingSourceFactory = createFactory(dataFlow),
         ).flow.cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot1 = pager.asSnapshot(this) {
@@ -347,12 +567,39 @@
     }
 
     @Test
-    fun append_outOfBounds_returnsCurrentlyLoadedItems() {
+    fun consecutivePrependWhile() {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = Pager(
+            config = CONFIG_NO_PREFETCH,
+            initialKey = 20,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot1 = pager.asSnapshot(this) {
+                prependScrollWhile { item: Int ->
+                    item > 17
+                }
+            }
+            assertThat(snapshot1).containsExactlyElementsIn(
+                listOf(17, 18, 19, 20, 21, 22, 23, 24)
+            )
+            val snapshot2 = pager.asSnapshot(this) {
+                prependScrollWhile { item: Int ->
+                    item > 11
+                }
+            }
+            assertThat(snapshot2).containsExactlyElementsIn(
+                listOf(11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24)
+            )
+        }
+    }
+
+    @Test
+    fun appendWhile_outOfBounds_returnsCurrentlyLoadedItems() {
         val dataFlow = flowOf(List(10) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
         val pager = Pager(
             config = CONFIG,
-            pagingSourceFactory = factory,
+            pagingSourceFactory = createFactory(dataFlow),
         )
         testScope.runTest {
             val snapshot = pager.flow.asSnapshot(this) {
@@ -370,12 +617,36 @@
     }
 
     @Test
-    fun refreshAndAppend() {
-        val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
+    fun prependWhile_outOfBounds_returnsCurrentlyLoadedItems() {
+        val dataFlow = flowOf(List(20) { it })
         val pager = Pager(
             config = CONFIG,
-            pagingSourceFactory = factory,
+            initialKey = 10,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot(this) {
+                prependScrollWhile { item: Int ->
+                    // condition scrolls till index = 0
+                    item > -3
+                }
+            }
+            // returns the items loaded before index becomes out of bounds
+            assertThat(snapshot).containsExactlyElementsIn(
+                // initial load [10-14]
+                // prefetched [7-9], [15-17]
+                // prepended [0-6]
+                listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
+            )
+        }
+    }
+
+    @Test
+    fun refreshAndAppendWhile() {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = Pager(
+            config = CONFIG,
+            pagingSourceFactory = createFactory(dataFlow),
         ).flow.cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot = pager.asSnapshot(this) {
@@ -391,12 +662,42 @@
     }
 
     @Test
-    fun appendAndRefresh() {
+    fun refreshAndPrependWhile() {
         val dataFlow = flowOf(List(30) { it })
-        val factory = dataFlow.asPagingSourceFactory(testScope)
         val pager = Pager(
             config = CONFIG,
-            pagingSourceFactory = factory,
+            initialKey = 20,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // this prependScrollWhile does not cause paging to load more items
+                // but it helps this test register a non-null anchorPosition so the upcoming
+                // refresh doesn't start at index 0
+                prependScrollWhile { item -> item > 20 }
+                // triggers second gen
+                refresh()
+                prependScrollWhile { item: Int ->
+                    item > 12
+                }
+            }
+            // second gen initial load, anchorPos = 20, refreshKey = 18, loaded
+            // initial load [18-22]
+            // prefetched [15-17], [23-25]
+            // prepended [12-14]
+            // prefetched [9-11]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25)
+            )
+        }
+    }
+
+    @Test
+    fun appendWhileAndRefresh() {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = Pager(
+            config = CONFIG,
+            pagingSourceFactory = createFactory(dataFlow),
         ).flow.cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot = pager.asSnapshot(this) {
@@ -407,17 +708,82 @@
             }
             assertThat(snapshot).containsExactlyElementsIn(
                 // second gen initial load, anchorPos = 10, refreshKey = 8
+                // initial load [8-12]
+                // prefetched [5-7], [13-15]
                 listOf(5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
             )
         }
     }
 
     @Test
-    fun consecutiveGenerations_fromSharedFlow() {
+    fun prependWhileAndRefresh() {
+        val dataFlow = flowOf(List(30) { it })
+        val pager = Pager(
+            config = CONFIG,
+            initialKey = 15,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                prependScrollWhile { item: Int ->
+                    item > 8
+                }
+                refresh()
+            }
+            assertThat(snapshot).containsExactlyElementsIn(
+                // second gen initial load, anchorPos = 8, refreshKey = 6
+                // initial load [6-10]
+                // prefetched [3-5], [11-13]
+                listOf(3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
+            )
+        }
+    }
+
+    @Test
+    fun consecutiveGenerations_fromFlow() {
+        val loadDelay = 500 + loadDelay
+        // wait for 500 + loadDelay between each emission
+        val dataFlow = flow {
+            emit(emptyList())
+            delay(loadDelay)
+
+            emit(List(30) { it })
+            delay(loadDelay)
+
+            emit(List(30) { it + 30 })
+        }
+        val pager = Pager(
+            config = CONFIG_NO_PREFETCH,
+            pagingSourceFactory = createFactory(dataFlow)
+        ).flow.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)
+            pagingSourceFactory = createFactory(dataFlow)
         ).flow.cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot1 = pager.asSnapshot(this) { }
@@ -442,37 +808,27 @@
     }
 
     @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 })
-        }
+    fun consecutiveGenerations_fromSharedFlow_emitBeforeRefresh() {
+        val dataFlow = MutableSharedFlow<List<Int>>()
         val pager = Pager(
             config = CONFIG_NO_PREFETCH,
-            pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+            pagingSourceFactory = createFactory(dataFlow)
         ).flow.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)
             )
@@ -480,18 +836,55 @@
     }
 
     @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,
+            pagingSourceFactory = createFactory(dataFlow)
+        ).flow.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 = Pager(
             config = CONFIG_NO_PREFETCH,
             initialKey = 10,
-            pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+            pagingSourceFactory = createFactory(dataFlow)
         ).flow.cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot1 = pager.asSnapshot(this) { }
@@ -499,10 +892,8 @@
                 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)
             )
@@ -511,17 +902,19 @@
 
     @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)
+            pagingSourceFactory = createFactory(dataFlow)
         ).flow.cachedIn(testScope.backgroundScope)
         testScope.runTest {
             val snapshot1 = pager.asSnapshot(this) {
@@ -534,9 +927,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)
@@ -544,11 +936,1111 @@
         }
     }
 
+    @Test
+    fun prependScroll() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = Pager(
+            config = CONFIG,
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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
+                )
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_withDrops() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = Pager(
+            config = CONFIG_WITH_DROPS,
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot(this) {
+                scrollTo(42)
+            }
+            // dropped [47-57]
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(38, 39, 40, 41, 42, 43, 44, 45, 46)
+            )
+        }
+    }
+
+    @Test
+    fun prependScroll_withSeparators() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = Pager(
+            config = CONFIG,
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 = Pager(
+            config = CONFIG,
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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 = Pager(
+            config = CONFIG,
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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.flow.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 = Pager(
+            config = CONFIG,
+            initialKey = 5,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 = Pager(
+            config = CONFIG,
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 = Pager(
+            config = CONFIG_NO_PREFETCH,
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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 = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            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-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 = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            initialKey = 5,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            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(-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 = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            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(-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 = Pager(
+            config = CONFIG,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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 = Pager(
+            config = CONFIG_WITH_DROPS,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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 = Pager(
+            config = CONFIG,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 = Pager(
+            config = CONFIG,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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 = Pager(
+            config = CONFIG,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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.flow.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 = Pager(
+            config = CONFIG,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 = Pager(
+            config = CONFIG,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 = Pager(
+            config = CONFIG_NO_PREFETCH,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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 = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.cachedIn(testScope.backgroundScope)
+        testScope.runTest {
+            val snapshot = pager.asSnapshot(this) {
+                // 13 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 = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 = Pager(
+            config = CONFIG,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow
+        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 = Pager(
+            config = CONFIG,
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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 = Pager(
+            config = CONFIG_WITH_DROPS,
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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 = Pager(
+            config = CONFIG,
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 = Pager(
+            config = CONFIG,
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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 = Pager(
+            config = CONFIG,
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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.flow.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 = Pager(
+            config = PagingConfig(
+                pageSize = 3,
+                initialLoadSize = 5,
+                jumpThreshold = 5
+            ),
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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 = Pager(
+            config = PagingConfig(
+                pageSize = 3,
+                initialLoadSize = 5,
+                jumpThreshold = 5
+            ),
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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_indexOutOfBounds() {
+        val dataFlow = flowOf(List(100) { it })
+        val pager = Pager(
+            config = CONFIG,
+            initialKey = 10,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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 = Pager(
+            config = CONFIG,
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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 = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            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]
+                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 = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            initialKey = 50,
+            pagingSourceFactory = createFactory(dataFlow),
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.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 = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            initialKey = 5,
+            pagingSourceFactory = createFactory(dataFlow),
+        ).flow.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 = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            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]
+                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 = Pager(
+            config = CONFIG_NO_PLACEHOLDERS,
+            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]
+                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)
+            )
+        }
+    }
+
     val CONFIG = PagingConfig(
         pageSize = 3,
         initialLoadSize = 5,
     )
 
+    val CONFIG_WITH_DROPS = PagingConfig(
+        pageSize = 3,
+        initialLoadSize = 5,
+        maxSize = 9
+    )
+
     val CONFIG_NO_PLACEHOLDERS = PagingConfig(
         pageSize = 3,
         initialLoadSize = 5,
@@ -561,4 +2053,30 @@
         initialLoadSize = 5,
         prefetchDistance = 0
     )
+}
+
+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/privacysandbox/ads/ads-adservices-java/api/res-current.txt b/privacysandbox/ads/ads-adservices-java/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/api/res-current.txt
diff --git a/privacysandbox/ads/ads-adservices-java/api/restricted_current.txt b/privacysandbox/ads/ads-adservices-java/api/restricted_current.txt
new file mode 100644
index 0000000..26eea8b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/api/restricted_current.txt
@@ -0,0 +1,92 @@
+// Signature format: 4.0
+package androidx.privacysandbox.ads.adservices.java.adid {
+
+  public abstract class AdIdManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_AD_ID) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.adid.AdId> getAdIdAsync();
+    field public static final androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures.Companion Companion;
+  }
+
+  public static final class AdIdManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.adselection {
+
+  public abstract class AdSelectionManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> reportImpressionAsync(androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest reportImpressionRequest);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome> selectAdsAsync(androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig adSelectionConfig);
+    field public static final androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures.Companion Companion;
+  }
+
+  public static final class AdSelectionManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.appsetid {
+
+  public abstract class AppSetIdManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures? from(android.content.Context context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.appsetid.AppSetId> getAppSetIdAsync();
+    field public static final androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures.Companion Companion;
+  }
+
+  public static final class AppSetIdManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.appsetid.AppSetIdManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.customaudience {
+
+  public abstract class CustomAudienceManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> joinCustomAudienceAsync(androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest request);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_CUSTOM_AUDIENCE) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> leaveCustomAudienceAsync(androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures.Companion Companion;
+  }
+
+  public static final class CustomAudienceManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.measurement {
+
+  public abstract class MeasurementManagerFutures {
+    method public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> deleteRegistrationsAsync(androidx.privacysandbox.ads.adservices.measurement.DeletionRequest deletionRequest);
+    method public static final androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Integer> getMeasurementApiStatusAsync();
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerSourceAsync(android.net.Uri attributionSource, android.view.InputEvent? inputEvent);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerTriggerAsync(android.net.Uri trigger);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerWebSourceAsync(androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest request);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_ATTRIBUTION) public abstract com.google.common.util.concurrent.ListenableFuture<kotlin.Unit> registerWebTriggerAsync(androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures.Companion Companion;
+  }
+
+  public static final class MeasurementManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures? from(android.content.Context context);
+  }
+
+}
+
+package androidx.privacysandbox.ads.adservices.java.topics {
+
+  public abstract class TopicsManagerFutures {
+    method public static final androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures? from(android.content.Context context);
+    method @RequiresPermission(android.adservices.common.AdServicesPermissions.ACCESS_ADSERVICES_TOPICS) public abstract com.google.common.util.concurrent.ListenableFuture<androidx.privacysandbox.ads.adservices.topics.GetTopicsResponse> getTopicsAsync(androidx.privacysandbox.ads.adservices.topics.GetTopicsRequest request);
+    field public static final androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures.Companion Companion;
+  }
+
+  public static final class TopicsManagerFutures.Companion {
+    method public androidx.privacysandbox.ads.adservices.java.topics.TopicsManagerFutures? from(android.content.Context context);
+  }
+
+}
+
diff --git a/privacysandbox/ads/ads-adservices-java/build.gradle b/privacysandbox/ads/ads-adservices-java/build.gradle
new file mode 100644
index 0000000..af73f43
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/build.gradle
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api(libs.kotlinStdlib)
+    api(libs.kotlinCoroutinesCore)
+    implementation("androidx.core:core-ktx:1.8.0")
+    api("androidx.annotation:annotation:1.2.0")
+
+    // To use CallbackToFutureAdapter
+    implementation "androidx.concurrent:concurrent-futures:1.1.0"
+    implementation(libs.guavaAndroid)
+    api(libs.guavaListenableFuture)
+    implementation project(path: ':privacysandbox:ads:ads-adservices')
+
+    androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.0'
+    androidTestImplementation project(path: ':privacysandbox:ads:ads-adservices')
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.kotlinTestJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.truth)
+
+    androidTestImplementation(libs.mockitoCore4, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(libs.dexmakerMockitoInline, excludes.bytebuddy) // DexMaker has it"s own MockMaker
+}
+
+android {
+    compileSdk = 33
+    compileSdkExtension = 4
+    namespace "androidx.privacysandbox.ads.adservices.java"
+}
+
+androidx {
+    name = "androidx.privacysandbox.ads:ads-adservices-java"
+    type = LibraryType.PUBLISHED_LIBRARY
+    inceptionYear = "2022"
+    description = "write Java code to call PP APIs."
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/AndroidManifest.xml b/privacysandbox/ads/ads-adservices-java/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..faff43e
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <uses-permission android:name="android.permission.ACCESS_ADSERVICES_TOPICS" />
+    <uses-permission android:name="android.permission.ACCESS_ADSERVICES_AD_ID" />
+    <uses-permission android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION" />
+    <application>
+        <property android:name="android.adservices.AD_SERVICES_CONFIG"
+            android:resource="@xml/ad_services_config" />
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt
new file mode 100644
index 0000000..bf0a81b
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adid/AdIdManagerFuturesTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ads.adservices.java.adid
+
+import android.content.Context
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.adid.AdId
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import com.google.common.util.concurrent.ListenableFuture
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class AdIdManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = Mockito.spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testAdIdOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Truth.assertThat(AdIdManagerFutures.from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testAdIdAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val adIdManager = mockAdIdManager(mContext)
+        setupResponse(adIdManager)
+        val managerCompat = AdIdManagerFutures.from(mContext)
+
+        // Actually invoke the compat code.
+        val result: ListenableFuture<AdId> = managerCompat!!.getAdIdAsync()
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result.get())
+
+        // Verify that the compat code was invoked correctly.
+        Mockito.verify(adIdManager).getAdId(ArgumentMatchers.any(), ArgumentMatchers.any())
+    }
+
+    @SuppressWarnings("NewApi")
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+
+        private fun mockAdIdManager(spyContext: Context): android.adservices.adid.AdIdManager {
+            val adIdManager = Mockito.mock(android.adservices.adid.AdIdManager::class.java)
+            Mockito.`when`(spyContext.getSystemService(
+                android.adservices.adid.AdIdManager::class.java)).thenReturn(adIdManager)
+            return adIdManager
+        }
+
+        private fun setupResponse(adIdManager: android.adservices.adid.AdIdManager) {
+            // Set up the response that AdIdManager will return when the compat code calls it.
+            val adId = android.adservices.adid.AdId("1234", false)
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<
+                    OutcomeReceiver<android.adservices.adid.AdId, Exception>>(1)
+                receiver.onResult(adId)
+                null
+            }
+            Mockito.doAnswer(answer)
+                .`when`(adIdManager).getAdId(
+                    ArgumentMatchers.any(),
+                    ArgumentMatchers.any()
+                )
+        }
+
+        private fun verifyResponse(adId: androidx.privacysandbox.ads.adservices.adid.AdId) {
+            Assert.assertEquals("1234", adId.adId)
+            Assert.assertEquals(false, adId.isLimitAdTrackingEnabled)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFuturesTest.kt
new file mode 100644
index 0000000..ff87882
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/adselection/AdSelectionManagerFuturesTest.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ads.adservices.java.adselection
+
+import android.content.Context
+import android.net.Uri
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionConfig
+import androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome
+import androidx.privacysandbox.ads.adservices.adselection.ReportImpressionRequest
+import androidx.test.core.app.ApplicationProvider
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.privacysandbox.ads.adservices.java.adselection.AdSelectionManagerFutures.Companion.from
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import com.google.common.util.concurrent.ListenableFuture
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class AdSelectionManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testAdSelectionOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Truth.assertThat(from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testSelectAds() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val adSelectionManager = mockAdSelectionManager(mContext)
+        setupAdSelectionResponse(adSelectionManager)
+        val managerCompat = from(mContext)
+
+        // Actually invoke the compat code.
+        val result: ListenableFuture<AdSelectionOutcome> =
+            managerCompat!!.selectAdsAsync(adSelectionConfig)
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result.get())
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.adselection.AdSelectionConfig::class.java)
+        verify(adSelectionManager).selectAds(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyRequest(captor.value)
+    }
+
+    @Test
+    @SuppressWarnings("NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testReportImpression() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val adSelectionManager = mockAdSelectionManager(mContext)
+        setupAdSelectionResponse(adSelectionManager)
+        val managerCompat = from(mContext)
+        val reportImpressionRequest = ReportImpressionRequest(adSelectionId, adSelectionConfig)
+
+        // Actually invoke the compat code.
+        managerCompat!!.reportImpressionAsync(reportImpressionRequest).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.adselection.ReportImpressionRequest::class.java)
+        verify(adSelectionManager).reportImpression(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyReportImpressionRequest(captor.value)
+    }
+
+    @SuppressWarnings("NewApi")
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+        private const val adSelectionId = 1234L
+        private const val adId = "1234"
+        private val seller: AdTechIdentifier = AdTechIdentifier(adId)
+        private val decisionLogicUri: Uri = Uri.parse("www.abc.com")
+        private val customAudienceBuyers: List<AdTechIdentifier> = listOf(seller)
+        private const val adSelectionSignalsStr = "adSelSignals"
+        private val adSelectionSignals: AdSelectionSignals =
+            AdSelectionSignals(adSelectionSignalsStr)
+        private const val sellerSignalsStr = "sellerSignals"
+        private val sellerSignals: AdSelectionSignals = AdSelectionSignals(sellerSignalsStr)
+        private val perBuyerSignals: Map<AdTechIdentifier, AdSelectionSignals> =
+            mutableMapOf(Pair(seller, sellerSignals))
+        private val trustedScoringSignalsUri: Uri = Uri.parse("www.xyz.com")
+        private val adSelectionConfig = AdSelectionConfig(
+            seller,
+            decisionLogicUri,
+            customAudienceBuyers,
+            adSelectionSignals,
+            sellerSignals,
+            perBuyerSignals,
+            trustedScoringSignalsUri)
+
+        // Response.
+        private val renderUri = Uri.parse("render-uri.com")
+
+        private fun mockAdSelectionManager(
+            spyContext: Context
+        ): android.adservices.adselection.AdSelectionManager {
+            val adSelectionManager =
+                mock(android.adservices.adselection.AdSelectionManager::class.java)
+            `when`(spyContext.getSystemService(
+                android.adservices.adselection.AdSelectionManager::class.java))
+                .thenReturn(adSelectionManager)
+            return adSelectionManager
+        }
+
+        private fun setupAdSelectionResponse(
+            adSelectionManager: android.adservices.adselection.AdSelectionManager
+        ) {
+            // Set up the response that AdSelectionManager will return when the compat code calls
+            // it.
+            val response = android.adservices.adselection.AdSelectionOutcome.Builder()
+                .setAdSelectionId(adSelectionId)
+                .setRenderUri(renderUri)
+                .build()
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<
+                    android.adservices.adselection.AdSelectionOutcome, Exception>>(2)
+                receiver.onResult(response)
+                null
+            }
+            doAnswer(answer)
+                .`when`(adSelectionManager).selectAds(
+                    any(),
+                    any(),
+                    any()
+                )
+
+            val answer2 = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+                receiver.onResult(Object())
+                null
+            }
+            doAnswer(answer2).`when`(adSelectionManager).reportImpression(any(), any(), any())
+        }
+
+        private fun verifyRequest(request: android.adservices.adselection.AdSelectionConfig) {
+            // Set up the request that we expect the compat code to invoke.
+            val expectedRequest = getPlatformAdSelectionConfig()
+
+            Assert.assertEquals(expectedRequest, request)
+        }
+
+        private fun verifyResponse(
+            outcome: androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome
+        ) {
+            val expectedOutcome =
+                androidx.privacysandbox.ads.adservices.adselection.AdSelectionOutcome(
+                    adSelectionId,
+                    renderUri)
+            Assert.assertEquals(expectedOutcome, outcome)
+        }
+
+        private fun getPlatformAdSelectionConfig():
+            android.adservices.adselection.AdSelectionConfig {
+            val adTechIdentifier = android.adservices.common.AdTechIdentifier.fromString(adId)
+            return android.adservices.adselection.AdSelectionConfig.Builder()
+                .setAdSelectionSignals(
+                    android.adservices.common.AdSelectionSignals.fromString(adSelectionSignalsStr))
+                .setCustomAudienceBuyers(listOf(adTechIdentifier))
+                .setDecisionLogicUri(decisionLogicUri)
+                .setPerBuyerSignals(mutableMapOf(Pair(
+                    adTechIdentifier,
+                    android.adservices.common.AdSelectionSignals.fromString(sellerSignalsStr))))
+                .setSeller(adTechIdentifier)
+                .setSellerSignals(
+                    android.adservices.common.AdSelectionSignals.fromString(sellerSignalsStr))
+                .setTrustedScoringSignalsUri(trustedScoringSignalsUri)
+                .build()
+        }
+
+        private fun verifyReportImpressionRequest(
+            request: android.adservices.adselection.ReportImpressionRequest
+        ) {
+            val expectedRequest = android.adservices.adselection.ReportImpressionRequest(
+                adSelectionId,
+                getPlatformAdSelectionConfig())
+            Assert.assertEquals(expectedRequest.adSelectionId, request.adSelectionId)
+            Assert.assertEquals(expectedRequest.adSelectionConfig, request.adSelectionConfig)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFuturesTest.kt
new file mode 100644
index 0000000..a558f76
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/appsetid/AppSetIdManagerFuturesTest.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ads.adservices.java.appsetid
+
+import android.content.Context
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.appsetid.AppSetId
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.ListenableFuture
+import org.junit.Assert
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class AppSetIdManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testAppSetIdOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        assertThat(AppSetIdManagerFutures.from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @SuppressWarnings("NewApi")
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testAppSetIdAsync() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val appSetIdManager = mockAppSetIdManager(mContext)
+        setupResponse(appSetIdManager)
+        val managerCompat = AppSetIdManagerFutures.from(mContext)
+
+        // Actually invoke the compat code.
+        val result: ListenableFuture<AppSetId> = managerCompat!!.getAppSetIdAsync()
+
+        // Verify that the result of the compat call is correct.
+        verifyResponse(result.get())
+
+        // Verify that the compat code was invoked correctly.
+        verify(appSetIdManager).getAppSetId(any(), any())
+    }
+
+    @SuppressWarnings("NewApi")
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+
+        private fun mockAppSetIdManager(
+            spyContext: Context
+        ): android.adservices.appsetid.AppSetIdManager {
+            val appSetIdManager = mock(android.adservices.appsetid.AppSetIdManager::class.java)
+            `when`(spyContext.getSystemService(
+                android.adservices.appsetid.AppSetIdManager::class.java))
+                .thenReturn(appSetIdManager)
+            return appSetIdManager
+        }
+
+        private fun setupResponse(appSetIdManager: android.adservices.appsetid.AppSetIdManager) {
+            // Set up the response that AdIdManager will return when the compat code calls it.
+            val appSetId = android.adservices.appsetid.AppSetId(
+                "1234",
+                android.adservices.appsetid.AppSetId.SCOPE_APP)
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<
+                    OutcomeReceiver<android.adservices.appsetid.AppSetId, Exception>>(1)
+                receiver.onResult(appSetId)
+                null
+            }
+            doAnswer(answer)
+                .`when`(appSetIdManager).getAppSetId(
+                    any(),
+                    any()
+                )
+        }
+
+        private fun verifyResponse(appSetId: AppSetId) {
+            Assert.assertEquals("1234", appSetId.id)
+            Assert.assertEquals(AppSetId.SCOPE_APP, appSetId.scope)
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFuturesTest.kt b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFuturesTest.kt
new file mode 100644
index 0000000..1ebf7e7
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/customaudience/CustomAudienceManagerFuturesTest.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ads.adservices.java.customaudience
+
+import android.adservices.customaudience.CustomAudienceManager
+import android.content.Context
+import android.net.Uri
+import android.os.OutcomeReceiver
+import android.os.ext.SdkExtensions
+import androidx.annotation.RequiresExtension
+import androidx.privacysandbox.ads.adservices.common.AdData
+import androidx.privacysandbox.ads.adservices.common.AdSelectionSignals
+import androidx.privacysandbox.ads.adservices.common.AdTechIdentifier
+import androidx.privacysandbox.ads.adservices.customaudience.CustomAudience
+import androidx.privacysandbox.ads.adservices.java.customaudience.CustomAudienceManagerFutures.Companion.from
+import androidx.privacysandbox.ads.adservices.customaudience.JoinCustomAudienceRequest
+import androidx.privacysandbox.ads.adservices.customaudience.LeaveCustomAudienceRequest
+import androidx.privacysandbox.ads.adservices.customaudience.TrustedBiddingData
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import java.time.Instant
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.invocation.InvocationOnMock
+
+@SmallTest
+@SuppressWarnings("NewApi")
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 30)
+class CustomAudienceManagerFuturesTest {
+
+    @Before
+    fun setUp() {
+        mContext = spy(ApplicationProvider.getApplicationContext<Context>())
+    }
+
+    @Test
+    @SdkSuppress(maxSdkVersion = 33, minSdkVersion = 30)
+    fun testOlderVersions() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("maxSdkVersion = API 33 ext 3", sdkExtVersion < 4)
+        Truth.assertThat(from(mContext)).isEqualTo(null)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testJoinCustomAudience() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val customAudienceManager = mockCustomAudienceManager(mContext)
+        setupResponse(customAudienceManager)
+        val managerCompat = from(mContext)
+
+        // Actually invoke the compat code.
+        val customAudience = CustomAudience.Builder(buyer, name, uri, uri, ads)
+            .setActivationTime(Instant.now())
+            .setExpirationTime(Instant.now())
+            .setUserBiddingSignals(userBiddingSignals)
+            .setTrustedBiddingData(trustedBiddingSignals)
+            .build()
+        val request = JoinCustomAudienceRequest(customAudience)
+        managerCompat!!.joinCustomAudienceAsync(request).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.customaudience.JoinCustomAudienceRequest::class.java
+        )
+        verify(customAudienceManager).joinCustomAudience(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyJoinCustomAudienceRequest(captor.value)
+    }
+
+    @Test
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    fun testLeaveCustomAudience() {
+        val sdkExtVersion = SdkExtensions.getExtensionVersion(SdkExtensions.AD_SERVICES)
+
+        Assume.assumeTrue("minSdkVersion = API 33 ext 4", sdkExtVersion >= 4)
+        val customAudienceManager = mockCustomAudienceManager(mContext)
+        setupResponse(customAudienceManager)
+        val managerCompat = from(mContext)
+
+        // Actually invoke the compat code.
+        val request = LeaveCustomAudienceRequest(buyer, name)
+        managerCompat!!.leaveCustomAudienceAsync(request).get()
+
+        // Verify that the compat code was invoked correctly.
+        val captor = ArgumentCaptor.forClass(
+            android.adservices.customaudience.LeaveCustomAudienceRequest::class.java
+        )
+        verify(customAudienceManager).leaveCustomAudience(captor.capture(), any(), any())
+
+        // Verify that the request that the compat code makes to the platform is correct.
+        verifyLeaveCustomAudienceRequest(captor.value)
+    }
+
+    @SdkSuppress(minSdkVersion = 30)
+    @RequiresExtension(extension = SdkExtensions.AD_SERVICES, version = 4)
+    companion object {
+        private lateinit var mContext: Context
+        private val uri: Uri = Uri.parse("abc.com")
+        private const val adtech = "1234"
+        private val buyer: AdTechIdentifier = AdTechIdentifier(adtech)
+        private const val name: String = "abc"
+        private const val signals = "signals"
+        private val userBiddingSignals: AdSelectionSignals = AdSelectionSignals(signals)
+        private val keys: List<String> = listOf("key1", "key2")
+        private val trustedBiddingSignals: TrustedBiddingData = TrustedBiddingData(uri, keys)
+        private const val metadata = "metadata"
+        private val ads: List<AdData> = listOf(AdData(uri, metadata))
+
+        private fun mockCustomAudienceManager(spyContext: Context): CustomAudienceManager {
+            val customAudienceManager = mock(CustomAudienceManager::class.java)
+            `when`(spyContext.getSystemService(CustomAudienceManager::class.java))
+                .thenReturn(customAudienceManager)
+            return customAudienceManager
+        }
+
+        private fun setupResponse(customAudienceManager: CustomAudienceManager) {
+            val answer = { args: InvocationOnMock ->
+                val receiver = args.getArgument<OutcomeReceiver<Any, Exception>>(2)
+                receiver.onResult(Object())
+                null
+            }
+            doAnswer(answer).`when`(customAudienceManager).joinCustomAudience(any(), any(), any())
+            doAnswer(answer).`when`(customAudienceManager).leaveCustomAudience(any(), any(), any())
+        }
+
+        private fun verifyJoinCustomAudienceRequest(
+            joinCustomAudienceRequest: android.adservices.customaudience.JoinCustomAudienceRequest
+        ) {
+            // Set up the request that we expect the compat code to invoke.
+            val adtechIdentifier = android.adservices.common.AdTechIdentifier.fromString(adtech)
+            val userBiddingSignals =
+                android.adservices.common.AdSelectionSignals.fromString(signals)
+            val trustedBiddingSignals =
+                android.adservices.customaudience.TrustedBiddingData.Builder()
+                    .setTrustedBiddingKeys(keys)
+                    .setTrustedBiddingUri(uri)
+                    .build()
+            val customAudience = android.adservices.customaudience.CustomAudience.Builder()
+                .setBuyer(adtechIdentifier)
+                .setName(name)
+                .setActivationTime(Instant.now())
+                .setExpirationTime(Instant.now())
+                .setBiddingLogicUri(uri)
+                .setDailyUpdateUri(uri)
+                .setUserBiddingSignals(userBiddingSignals)
+                .setTrustedBiddingData(trustedBiddingSignals)
+                .setAds(listOf(android.adservices.common.AdData.Builder()
+                    .setRenderUri(uri)
+                    .setMetadata(metadata)
+                    .build()))
+                .build()
+
+            val expectedRequest =
+                android.adservices.customaudience.JoinCustomAudienceRequest.Builder()
+                    .setCustomAudience(customAudience)
+                    .build()
+
+            // Verify that the actual request matches the expected one.
+            Truth.assertThat(expectedRequest.customAudience.ads.size ==
+                joinCustomAudienceRequest.customAudience.ads.size).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.ads[0].renderUri ==
+                joinCustomAudienceRequest.customAudience.ads[0].renderUri).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.ads[0].metadata ==
+                joinCustomAudienceRequest.customAudience.ads[0].metadata).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.biddingLogicUri ==
+                joinCustomAudienceRequest.customAudience.biddingLogicUri).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.buyer.toString() ==
+                joinCustomAudienceRequest.customAudience.buyer.toString()).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.dailyUpdateUri ==
+                joinCustomAudienceRequest.customAudience.dailyUpdateUri).isTrue()
+            Truth.assertThat(expectedRequest.customAudience.name ==
+                joinCustomAudienceRequest.customAudience.name).isTrue()
+            Truth.assertThat(trustedBiddingSignals.trustedBiddingKeys ==
+                joinCustomAudienceRequest.customAudience.trustedBiddingData!!.trustedBiddingKeys)
+                .isTrue()
+            Truth.assertThat(trustedBiddingSignals.trustedBiddingUri ==
+                joinCustomAudienceRequest.customAudience.trustedBiddingData!!.trustedBiddingUri)
+                .isTrue()
+            Truth.assertThat(
+                joinCustomAudienceRequest.customAudience.userBiddingSignals!!.toString() ==
+                signals).isTrue()
+        }
+
+        private fun verifyLeaveCustomAudienceRequest(
+            leaveCustomAudienceRequest: android.adservices.customaudience.LeaveCustomAudienceRequest
+        ) {
+            // Set up the request that we expect the compat code to invoke.
+            val adtechIdentifier = android.adservices.common.AdTechIdentifier.fromString(adtech)
+
+            val expectedRequest = android.adservices.customaudience.LeaveCustomAudienceRequest
+                .Builder()
+                .setBuyer(adtechIdentifier)
+                .setName(name)
+                .build()
+
+            // Verify that the actual request matches the expected one.
+            Truth.assertThat(expectedRequest == leaveCustomAudienceRequest).isTrue()
+        }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
new file mode 100644
index 0000000..8d5d99a
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/TestUtil.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ads.adservices.java.endtoend;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import java.util.List;
+
+public class TestUtil {
+    private Instrumentation mInstrumentation;
+    private String mTag;
+    // Used to get the package name. Copied over from com.android.adservices.AdServicesCommon
+    private static final String TOPICS_SERVICE_NAME = "android.adservices.TOPICS_SERVICE";
+    // The JobId of the Epoch Computation.
+    private static final int EPOCH_JOB_ID = 2;
+
+    public TestUtil(Instrumentation instrumentation, String tag) {
+        mInstrumentation = instrumentation;
+        mTag = tag;
+    }
+    // Run shell command.
+    private void runShellCommand(String command) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                mInstrumentation.getUiAutomation().executeShellCommand(command);
+            }
+        }
+    }
+    public void overrideKillSwitches(boolean override) {
+        if (override) {
+            runShellCommand("setprop debug.adservices.global_kill_switch " + false);
+            runShellCommand("setprop debug.adservices.topics_kill_switch " + false);
+        } else {
+            runShellCommand("setprop debug.adservices.global_kill_switch " + null);
+            runShellCommand("setprop debug.adservices.topics_kill_switch " + null);
+        }
+    }
+
+    public void enableEnrollmentCheck(boolean enable) {
+        runShellCommand(
+                "setprop debug.adservices.disable_topics_enrollment_check " + enable);
+    }
+
+    // Override the Epoch Period to shorten the Epoch Length in the test.
+    public void overrideEpochPeriod(long overrideEpochPeriod) {
+        runShellCommand(
+                "setprop debug.adservices.topics_epoch_job_period_ms " + overrideEpochPeriod);
+    }
+
+    // Override the Percentage For Random Topic in the test.
+    public void overridePercentageForRandomTopic(long overridePercentage) {
+        runShellCommand(
+                "setprop debug.adservices.topics_percentage_for_random_topics "
+                        + overridePercentage);
+    }
+
+    /** Forces JobScheduler to run the Epoch Computation job */
+    public void forceEpochComputationJob() {
+        runShellCommand(
+                "cmd jobscheduler run -f" + " " + getAdServicesPackageName() + " " + EPOCH_JOB_ID);
+    }
+
+    public void overrideConsentManagerDebugMode(boolean override) {
+        String overrideStr = override ? "true" : "null";
+        runShellCommand("setprop debug.adservices.consent_manager_debug_mode " + overrideStr);
+    }
+
+    public void overrideAllowlists(boolean override) {
+        String overrideStr = override ? "*" : "null";
+        runShellCommand("device_config put adservices ppapi_app_allow_list " + overrideStr);
+        runShellCommand("device_config put adservices ppapi_app_signature_allow_list "
+                + overrideStr);
+        runShellCommand(
+                "device_config put adservices web_context_client_allow_list " + overrideStr);
+    }
+
+    public void overrideAdIdKillSwitch(boolean override) {
+        if (override) {
+            runShellCommand("setprop debug.adservices.adid_kill_switch " + false);
+        } else {
+            runShellCommand("setprop debug.adservices.adid_kill_switch " + null);
+        }
+    }
+
+    // Override measurement related kill switch to ignore the effect of actual PH values.
+    // If isOverride = true, override measurement related kill switch to OFF to allow adservices
+    // If isOverride = false, override measurement related kill switch to meaningless value so that
+    // PhFlags will use the default value.
+    public void overrideMeasurementKillSwitches(boolean isOverride) {
+        String overrideString = isOverride ? "false" : "null";
+        runShellCommand("setprop debug.adservices.global_kill_switch " + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_kill_switch " + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_register_source_kill_switch "
+                + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_register_trigger_kill_switch "
+                + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_register_web_source_kill_switch "
+                + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_register_web_trigger_kill_switch "
+                + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_delete_registrations_kill_switch "
+                + overrideString);
+        runShellCommand("setprop debug.adservices.measurement_api_status_kill_switch "
+                + overrideString);
+    }
+
+    // Override the flag to disable Measurement enrollment check. Setting to 1 disables enforcement.
+    public void overrideDisableMeasurementEnrollmentCheck(String val) {
+        runShellCommand("setprop debug.adservices.disable_measurement_enrollment_check " + val);
+    }
+
+    public void resetOverrideDisableMeasurementEnrollmentCheck() {
+        runShellCommand("setprop debug.adservices.disable_measurement_enrollment_check null");
+    }
+
+    @SuppressWarnings("deprecation")
+    // Used to get the package name. Copied over from com.android.adservices.AndroidServiceBinder
+    public String getAdServicesPackageName() {
+        final Intent intent = new Intent(TOPICS_SERVICE_NAME);
+        final List<ResolveInfo> resolveInfos = ApplicationProvider.getApplicationContext()
+                .getPackageManager()
+                .queryIntentServices(intent, PackageManager.MATCH_SYSTEM_ONLY);
+
+        if (resolveInfos == null || resolveInfos.isEmpty()) {
+            Log.e(mTag, "Failed to find resolveInfo for adServices service. Intent action: "
+                            + TOPICS_SERVICE_NAME);
+            return null;
+        }
+
+        if (resolveInfos.size() > 1) {
+            String str = String.format(
+                    "Found multiple services (%1$s) for the same intent action (%2$s)",
+                    TOPICS_SERVICE_NAME, resolveInfos);
+            Log.e(mTag, str);
+            return null;
+        }
+
+        final ServiceInfo serviceInfo = resolveInfos.get(0).serviceInfo;
+        if (serviceInfo == null) {
+            Log.e(mTag, "Failed to find serviceInfo for adServices service");
+            return null;
+        }
+
+        return serviceInfo.packageName;
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java
new file mode 100644
index 0000000..20294a1
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/adid/AdIdManagerTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ads.adservices.java.endtoend.adid;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.privacysandbox.ads.adservices.adid.AdId;
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo;
+import androidx.privacysandbox.ads.adservices.java.adid.AdIdManagerFutures;
+import androidx.privacysandbox.ads.adservices.java.endtoend.TestUtil;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+
+@RunWith(JUnit4.class)
+public class AdIdManagerTest {
+    private static final String TAG = "AdIdManagerTest";
+    private TestUtil mTestUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+
+    @Before
+    public void setup() throws Exception {
+        mTestUtil.overrideAdIdKillSwitch(true);
+        mTestUtil.overrideKillSwitches(true);
+        mTestUtil.overrideConsentManagerDebugMode(true);
+        mTestUtil.overrideAllowlists(true);
+    }
+
+    @After
+    public void teardown() {
+        mTestUtil.overrideAdIdKillSwitch(false);
+        mTestUtil.overrideKillSwitches(false);
+        mTestUtil.overrideConsentManagerDebugMode(false);
+        mTestUtil.overrideAllowlists(false);
+    }
+
+    @Test
+    public void testAdId() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        AdIdManagerFutures adIdManager =
+                AdIdManagerFutures.from(ApplicationProvider.getApplicationContext());
+        AdId adId = adIdManager.getAdIdAsync().get();
+        assertThat(adId.getAdId()).isNotEmpty();
+        assertThat(adId.isLimitAdTrackingEnabled()).isNotNull();
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java
new file mode 100644
index 0000000..2cb7889
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/measurement/MeasurementManagerTest.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ads.adservices.java.endtoend.measurement;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.net.Uri;
+
+import androidx.privacysandbox.ads.adservices.internal.AdServicesInfo;
+import androidx.privacysandbox.ads.adservices.java.endtoend.TestUtil;
+import androidx.privacysandbox.ads.adservices.java.measurement.MeasurementManagerFutures;
+import androidx.privacysandbox.ads.adservices.measurement.DeletionRequest;
+import androidx.privacysandbox.ads.adservices.measurement.WebSourceParams;
+import androidx.privacysandbox.ads.adservices.measurement.WebSourceRegistrationRequest;
+import androidx.privacysandbox.ads.adservices.measurement.WebTriggerParams;
+import androidx.privacysandbox.ads.adservices.measurement.WebTriggerRegistrationRequest;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(JUnit4.class)
+// TODO: Consider refactoring so that we're not duplicating code.
+public class MeasurementManagerTest {
+    private static final String TAG = "MeasurementManagerTest";
+    TestUtil mTestUtil = new TestUtil(InstrumentationRegistry.getInstrumentation(), TAG);
+
+    /* Note: The source and trigger registration used here must match one of those in
+       {@link PreEnrolledAdTechForTest}.
+    */
+    private static final Uri SOURCE_REGISTRATION_URI = Uri.parse("https://test.com/source");
+    private static final Uri TRIGGER_REGISTRATION_URI = Uri.parse("https://test.com/trigger");
+    private static final Uri DESTINATION = Uri.parse("http://trigger-origin.com");
+    private static final Uri OS_DESTINATION = Uri.parse("android-app://com.os.destination");
+    private static final Uri WEB_DESTINATION = Uri.parse("http://web-destination.com");
+    private static final Uri ORIGIN_URI = Uri.parse("https://sample.example1.com");
+    private static final Uri DOMAIN_URI = Uri.parse("https://example2.com");
+
+    private MeasurementManagerFutures mMeasurementManager;
+
+    @Before
+    public void setup() {
+        // To grant access to all pp api app
+        mTestUtil.overrideAllowlists(true);
+        // We need to turn the Consent Manager into debug mode
+        mTestUtil.overrideConsentManagerDebugMode(true);
+        mTestUtil.overrideMeasurementKillSwitches(true);
+        mTestUtil.overrideDisableMeasurementEnrollmentCheck("1");
+        mMeasurementManager =
+                MeasurementManagerFutures.from(ApplicationProvider.getApplicationContext());
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mTestUtil.overrideAllowlists(false);
+        mTestUtil.overrideConsentManagerDebugMode(false);
+        mTestUtil.resetOverrideDisableMeasurementEnrollmentCheck();
+        mTestUtil.overrideMeasurementKillSwitches(false);
+        mTestUtil.overrideDisableMeasurementEnrollmentCheck("0");
+        // Cool-off rate limiter
+        TimeUnit.SECONDS.sleep(1);
+    }
+
+    @Test
+    public void testRegisterSource_NoServerSetup_NoErrors() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        assertThat(mMeasurementManager.registerSourceAsync(
+                SOURCE_REGISTRATION_URI,
+                /* inputEvent= */ null).get())
+                .isNotNull();
+    }
+
+    @Test
+    public void testRegisterTrigger_NoServerSetup_NoErrors() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        assertThat(mMeasurementManager.registerTriggerAsync(TRIGGER_REGISTRATION_URI).get())
+                .isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void registerWebSource_NoErrors() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        WebSourceParams webSourceParams =
+                new WebSourceParams(SOURCE_REGISTRATION_URI, false);
+
+        WebSourceRegistrationRequest webSourceRegistrationRequest =
+                new WebSourceRegistrationRequest(
+                        Collections.singletonList(webSourceParams),
+                        SOURCE_REGISTRATION_URI,
+                        /* inputEvent= */ null,
+                        OS_DESTINATION,
+                        WEB_DESTINATION,
+                        /* verifiedDestination= */ null);
+
+        assertThat(mMeasurementManager.registerWebSourceAsync(webSourceRegistrationRequest).get())
+                .isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void registerWebTrigger_NoErrors() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        WebTriggerParams webTriggerParams =
+                new WebTriggerParams(TRIGGER_REGISTRATION_URI, false);
+        WebTriggerRegistrationRequest webTriggerRegistrationRequest =
+                new WebTriggerRegistrationRequest(
+                        Collections.singletonList(webTriggerParams),
+                        DESTINATION);
+
+        assertThat(mMeasurementManager.registerWebTriggerAsync(webTriggerRegistrationRequest).get())
+                .isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void testDeleteRegistrations_withRequest_withNoRange_withCallback_NoErrors()
+            throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        DeletionRequest deletionRequest =
+                new DeletionRequest.Builder(
+                        DeletionRequest.DELETION_MODE_ALL,
+                        DeletionRequest.MATCH_BEHAVIOR_DELETE)
+                        .setDomainUris(Collections.singletonList(DOMAIN_URI))
+                        .setOriginUris(Collections.singletonList(ORIGIN_URI))
+                        .build();
+        assertThat(mMeasurementManager.deleteRegistrationsAsync(deletionRequest).get())
+                .isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void testDeleteRegistrations_withRequest_withEmptyLists_withRange_withCallback_NoErrors()
+            throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        DeletionRequest deletionRequest =
+                new DeletionRequest.Builder(
+                        DeletionRequest.DELETION_MODE_ALL,
+                        DeletionRequest.MATCH_BEHAVIOR_DELETE)
+                        .setDomainUris(Collections.singletonList(DOMAIN_URI))
+                        .setOriginUris(Collections.singletonList(ORIGIN_URI))
+                        .setStart(Instant.ofEpochMilli(0))
+                        .setEnd(Instant.now())
+                        .build();
+        assertThat(mMeasurementManager.deleteRegistrationsAsync(deletionRequest).get())
+                .isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void testDeleteRegistrations_withRequest_withInvalidArguments_withCallback_hasError()
+            throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        DeletionRequest deletionRequest =
+                new DeletionRequest.Builder(
+                        DeletionRequest.DELETION_MODE_ALL,
+                        DeletionRequest.MATCH_BEHAVIOR_DELETE)
+                        .setDomainUris(Collections.singletonList(DOMAIN_URI))
+                        .setOriginUris(Collections.singletonList(ORIGIN_URI))
+                        .setStart(Instant.now().plusMillis(1000))
+                        .setEnd(Instant.now())
+                        .build();
+        Exception exception = assertThrows(
+                ExecutionException.class,
+                () ->
+                mMeasurementManager.deleteRegistrationsAsync(deletionRequest).get());
+        assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33)
+    public void testMeasurementApiStatus_returnResultStatus() throws Exception {
+        // Skip the test if SDK extension 4 is not present.
+        Assume.assumeTrue(AdServicesInfo.INSTANCE.version() >= 4);
+
+        int result = mMeasurementManager.getMeasurementApiStatusAsync().get();
+        assertThat(result).isEqualTo(1);
+    }
+}
diff --git a/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java b/privacysandbox/ads/ads-adservices-java/src/androidTest/java/androidx/privacysandbox/ads/adservices/java/endtoend/topics/TopicsManagerTest.java
new file mode 100644
index 0000000..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/privacysandbox/ads/ads-adservices/api/res-current.txt b/privacysandbox/ads/ads-adservices/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/privacysandbox/ads/ads-adservices/api/res-current.txt
diff --git a/privacysandbox/ads/ads-adservices/api/restricted_current.txt b/privacysandbox/ads/ads-adservices/api/restricted_current.txt
new file mode 100644
index 0000000..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/profileinstaller/profileinstaller/api/res-1.3.0-beta01.txt b/profileinstaller/profileinstaller/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/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/test/SuspendingQueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
index d5a9334..675a185 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
@@ -18,6 +18,8 @@
 
 import android.content.Context
 import android.os.Build
+import android.os.StrictMode
+import android.os.StrictMode.ThreadPolicy
 import androidx.arch.core.executor.ArchTaskExecutor
 import androidx.arch.core.executor.TaskExecutor
 import androidx.room.Room
@@ -33,6 +35,7 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import java.io.IOException
@@ -153,6 +156,31 @@
     }
 
     @Test
+    fun allBookSuspend_autoClose() {
+        val context: Context = ApplicationProvider.getApplicationContext()
+        context.deleteDatabase("autoClose.db")
+        val db = Room.databaseBuilder(
+            context = context,
+            klass = TestDatabase::class.java,
+            name = "test.db"
+        ).setAutoCloseTimeout(10, TimeUnit.MILLISECONDS).build()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            StrictMode.setThreadPolicy(
+                ThreadPolicy.Builder()
+                    .detectDiskReads()
+                    .detectDiskWrites()
+                    .penaltyDeath()
+                    .build()
+            )
+            runBlocking {
+                db.booksDao().getBooksSuspend()
+                delay(100) // let db auto-close
+                db.booksDao().getBooksSuspend()
+            }
+        }
+    }
+
+    @Test
     @Suppress("DEPRECATION")
     fun suspendingBlock_beginEndTransaction() {
         runBlocking {
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 639ba0d..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")
                     )
                 )
@@ -1681,13 +1679,14 @@
     @Test
     fun dataClass_primaryConstructor() {
         listOf(
-            TestData.AllDefaultVals::class.java.canonicalName!!,
-            TestData.AllDefaultVars::class.java.canonicalName!!,
-            TestData.SomeDefaultVals::class.java.canonicalName!!,
-            TestData.SomeDefaultVars::class.java.canonicalName!!,
-            TestData.WithJvmOverloads::class.java.canonicalName!!
+            "foo.bar.TestData.AllDefaultVals",
+            "foo.bar.TestData.AllDefaultVars",
+            "foo.bar.TestData.SomeDefaultVals",
+            "foo.bar.TestData.SomeDefaultVars",
+            "foo.bar.TestData.WithJvmOverloads"
         ).forEach {
-            runProcessorTest { invocation ->
+            runProcessorTest(sources = listOf(TEST_DATA)) {
+                    invocation ->
                 PojoProcessor.createFor(
                     context = invocation.context,
                     element = invocation.processingEnv.requireTypeElement(it),
@@ -1703,11 +1702,11 @@
 
     @Test
     fun dataClass_withJvmOverloads_primaryConstructor() {
-        runProcessorTest { invocation ->
+        runProcessorTest(sources = listOf(TEST_DATA)) { invocation ->
             PojoProcessor.createFor(
                 context = invocation.context,
                 element = invocation.processingEnv.requireTypeElement(
-                    TestData.WithJvmOverloads::class
+                    "foo.bar.TestData.WithJvmOverloads"
                 ),
                 bindingScope = FieldProcessor.BindingScope.READ_FROM_CURSOR,
                 parent = null
@@ -1721,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;
             }
@@ -1751,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;
                         }
@@ -1788,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() {
@@ -1823,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;
@@ -1853,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;
                     }
@@ -1887,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; }
@@ -1915,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; }
@@ -1948,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; }
@@ -1981,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; }
@@ -2012,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; }
@@ -2043,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; }
@@ -2142,9 +2141,9 @@
     @Test
     fun embedded_nullability() {
         listOf(
-            TestData.SomeEmbeddedVals::class.java.canonicalName!!
+            "foo.bar.TestData.SomeEmbeddedVals"
         ).forEach {
-            runProcessorTest { invocation ->
+            runProcessorTest(sources = listOf(TEST_DATA)) { invocation ->
                 val result = PojoProcessor.createFor(
                     context = invocation.context,
                     element = invocation.processingEnv.requireTypeElement(it),
@@ -2187,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,
@@ -2206,48 +2205,47 @@
     }
 
     // Kotlin data classes to verify the PojoProcessor.
-    private class TestData {
-        data class AllDefaultVals(
-            val name: String = "",
-            val number: Int = 0,
-            val bit: Boolean = false
-        )
-
-        data class AllDefaultVars(
-            var name: String = "",
-            var number: Int = 0,
-            var bit: Boolean = false
-        )
-
-        data class SomeDefaultVals(
-            val name: String,
-            val number: Int = 0,
-            val bit: Boolean
-        )
-
-        data class SomeDefaultVars(
-            var name: String,
-            var number: Int = 0,
-            var bit: Boolean
-        )
-
-        data class WithJvmOverloads @JvmOverloads constructor(
-            val name: String,
-            val lastName: String = "",
-            var number: Int = 0,
-            var bit: Boolean
-        )
-
-        data class AllNullableVals(
-            val name: String?,
-            val number: Int?,
-            val bit: Boolean?
-        )
-
-        data class SomeEmbeddedVals(
-            val id: String,
-            @Embedded(prefix = "non_nullable_") val nonNullableVal: AllNullableVals,
-            @Embedded(prefix = "nullable_") val nullableVal: AllNullableVals?
-        )
-    }
-}
+    private val TEST_DATA = Source.kotlin("TestData.kt", """
+        package foo.bar
+        import ${Embedded::class.java.canonicalName}
+        private class TestData {
+            data class AllDefaultVals(
+                val name: String = "",
+                val number: Int = 0,
+                val bit: Boolean = false
+            )
+            data class AllDefaultVars(
+                var name: String = "",
+                var number: Int = 0,
+                var bit: Boolean = false
+            )
+            data class SomeDefaultVals(
+                val name: String,
+                val number: Int = 0,
+                val bit: Boolean
+            )
+            data class SomeDefaultVars(
+                var name: String,
+                var number: Int = 0,
+                var bit: Boolean
+            )
+            data class WithJvmOverloads @JvmOverloads constructor(
+                val name: String,
+                val lastName: String = "",
+                var number: Int = 0,
+                var bit: Boolean
+            )
+            data class AllNullableVals(
+                val name: String?,
+                val number: Int?,
+                val bit: Boolean?
+            )
+            data class SomeEmbeddedVals(
+                val id: String,
+                @Embedded(prefix = "non_nullable_") val nonNullableVal: AllNullableVals,
+                @Embedded(prefix = "nullable_") val nullableVal: AllNullableVals?
+            )
+        }
+        """
+    )
+}
\ No newline at end of file
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-ktx/src/main/java/androidx/room/CoroutinesRoom.kt b/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
index 1495051e..69c458d 100644
--- a/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
+++ b/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
@@ -53,7 +53,7 @@
             inTransaction: Boolean,
             callable: Callable<R>
         ): R {
-            if (db.isOpen && db.inTransaction()) {
+            if (db.isOpenInternal && db.inTransaction()) {
                 return callable.call()
             }
 
@@ -74,7 +74,7 @@
             cancellationSignal: CancellationSignal?,
             callable: Callable<R>
         ): R {
-            if (db.isOpen && db.inTransaction()) {
+            if (db.isOpenInternal && db.inTransaction()) {
                 return callable.call()
             }
 
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/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt b/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
index 251b82a..2088242 100644
--- a/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
+++ b/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
@@ -162,11 +162,11 @@
         }
     }
 
-    // TODO: Close CleanupStatement
-    internal fun onAutoCloseCallback() {
+    private fun onAutoCloseCallback() {
         synchronized(trackerLock) {
             initialized = false
             observedTableTracker.resetTriggerState()
+            cleanupStatement?.close()
         }
     }
 
@@ -329,7 +329,7 @@
     }
 
     internal fun ensureInitialization(): Boolean {
-        if (!database.isOpen) {
+        if (!database.isOpenInternal) {
             return false
         }
         if (!initialized) {
@@ -526,7 +526,7 @@
      * This api should eventually be public.
      */
     internal fun syncTriggers() {
-        if (!database.isOpen) {
+        if (!database.isOpenInternal) {
             return
         }
         syncTriggers(database.openHelper.writableDatabase)
diff --git a/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt b/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt
index 599d9d3..a455992 100644
--- a/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt
+++ b/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt
@@ -394,13 +394,22 @@
     /**
      * True if database connection is open and initialized.
      *
+     * When Room is configured with [RoomDatabase.Builder.setAutoCloseTimeout] the database
+     * is considered open even if internally the connection has been closed, unless manually closed.
+     *
      * @return true if the database connection is open, false otherwise.
      */
-    @Suppress("Deprecation")
+    @Suppress("Deprecation") // Due to usage of `mDatabase`
     open val isOpen: Boolean
-        get() {
-            return (autoCloser?.isActive ?: mDatabase?.isOpen) == true
-        }
+        get() = (autoCloser?.isActive ?: mDatabase?.isOpen) == true
+
+    /**
+     * True if the actual database connection is open, regardless of auto-close.
+     */
+    @Suppress("Deprecation") // Due to usage of `mDatabase`
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    val isOpenInternal: Boolean
+        get() = mDatabase?.isOpen == true
 
     /**
      * Closes the database if it is already open.
@@ -1193,7 +1202,7 @@
 
         /**
          * Enables auto-closing for the database to free up unused resources. The underlying
-         * database will be closed after it's last use after the specified `autoCloseTimeout` has
+         * database will be closed after it's last use after the specified [autoCloseTimeout] has
          * elapsed since its last usage. The database will be automatically
          * re-opened the next time it is accessed.
          *
@@ -1210,7 +1219,7 @@
          *
          * The auto-closing database operation runs on the query executor.
          *
-         * The database will not be reopened if the RoomDatabase or the
+         * The database will not be re-opened if the RoomDatabase or the
          * SupportSqliteOpenHelper is closed manually (by calling
          * [RoomDatabase.close] or [SupportSQLiteOpenHelper.close]. If the
          * database is closed manually, you must create a new database using
diff --git a/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt b/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt
index 5f2c9aa..060110e 100644
--- a/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt
+++ b/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt
@@ -77,7 +77,7 @@
         doReturn(statement).whenever(mSqliteDb)
             .compileStatement(eq(InvalidationTracker.RESET_UPDATED_TABLES_SQL))
         doReturn(mSqliteDb).whenever(mOpenHelper).writableDatabase
-        doReturn(true).whenever(mRoomDatabase).isOpen
+        doReturn(true).whenever(mRoomDatabase).isOpenInternal
         doReturn(ArchTaskExecutor.getIOThreadExecutor()).whenever(mRoomDatabase).queryExecutor
         val closeLock = ReentrantLock()
         doReturn(closeLock).whenever(mRoomDatabase).getCloseLock()
@@ -247,7 +247,7 @@
 
     @Test
     fun closedDb() {
-        doReturn(false).whenever(mRoomDatabase).isOpen
+        doReturn(false).whenever(mRoomDatabase).isOpenInternal
         doThrow(IllegalStateException("foo")).whenever(mOpenHelper).writableDatabase
         mTracker.addObserver(LatchObserver(1, "a", "b"))
         mTracker.refreshRunnable.run()
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/settings.gradle b/settings.gradle
index cf57a94..9e0ec06 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])
@@ -579,7 +594,8 @@
 includeProject(":concurrent:concurrent-futures", [BuildType.MAIN, BuildType.CAMERA, BuildType.COMPOSE])
 includeProject(":concurrent:concurrent-futures-ktx", [BuildType.MAIN, BuildType.CAMERA])
 includeProject(":constraintlayout:constraintlayout-compose", [BuildType.COMPOSE])
-includeProject(":constraintlayout:constraintlayout-compose:integration-tests:constraintlayout-compose-demos", [BuildType.COMPOSE])
+includeProject(":constraintlayout:constraintlayout-compose-lint", [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])
@@ -707,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-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])
-includeProject(":lifecycle:lifecycle-viewmodel", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
+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])
@@ -794,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])
@@ -911,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])
@@ -959,19 +978,21 @@
 includeProject(":wear:watchface:watchface-samples-minimal-instances", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:watchface:watchface-samples-minimal-style", [BuildType.MAIN, BuildType.WEAR])
 includeProject(":wear:watchface:watchface-style", [BuildType.MAIN, BuildType.WEAR])
+includeProject(":wear:watchface:watchface-style-old-api-test-service", "wear/watchface/watchface-style/old-api-test-service", [BuildType.MAIN, BuildType.WEAR])
+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])
@@ -1019,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/build.gradle b/slice/slice-view/build.gradle
index 8d7bd57..1df018c 100644
--- a/slice/slice-view/build.gradle
+++ b/slice/slice-view/build.gradle
@@ -35,7 +35,7 @@
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.testRules)
     androidTestImplementation(libs.espressoCore, excludes.espresso)
-    androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
+    androidTestImplementation(libs.mockitoCore4, excludes.bytebuddy)
     androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
 }
 
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 38784fa..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,13 +36,13 @@
 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;
 import androidx.slice.widget.SliceLiveData;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
 
@@ -110,10 +110,13 @@
         verify(mSliceProvider, timeout(2000)).onSliceUnpinned(eq(uri));
     }
 
-    @FlakyTest(bugId = 239964752)
     @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 fdfd994..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 {
@@ -78,6 +67,7 @@
         }
     }
 
+    @Ignore // b/264940470
     @Test
     @SdkSuppress(minSdkVersion = 24)
     public void testMultiWindow_pictureInPicture() {
@@ -98,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.
@@ -116,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/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc b/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc
index 68efd0c..72776fc 100644
--- a/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc
+++ b/tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc
@@ -25,7 +25,7 @@
 // Concept of version useful e.g. for human-readable error messages, and stable once released.
 // Does not replace the need for a binary verification mechanism (e.g. checksum check).
 // TODO: populate using CMake
-#define VERSION "1.0.0-alpha09"
+#define VERSION "1.0.0-alpha10"
 
 namespace tracing_perfetto {
     void RegisterWithPerfetto() {
diff --git a/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt b/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt
index 66a4743..7178a50 100644
--- a/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt
+++ b/tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/perfetto/jni/test/PerfettoNativeTest.kt
@@ -30,7 +30,7 @@
         init {
             PerfettoNative.loadLib()
         }
-        const val libraryVersion = "1.0.0-alpha09" // TODO: get using reflection
+        const val libraryVersion = "1.0.0-alpha10" // TODO: get using reflection
     }
 
     @Test
diff --git a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
index efa3607..5278ec7 100644
--- a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
+++ b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
@@ -25,12 +25,12 @@
 
     // TODO(224510255): load from a file produced at build time
     object Metadata {
-        const val version = "1.0.0-alpha09"
+        const val version = "1.0.0-alpha10"
         val checksums = mapOf(
-            "arm64-v8a" to "130cea2d90e941acb9d2087c0bebe8229b461175454df20763bfe901c0d73155",
-            "armeabi-v7a" to "cb0e60c5d8726d274bbc678cfa3a8724e66d2bfa6874c06c5f4347e545c0439d",
-            "x86" to "c6b19ce773d17c74aa74aa813637bc1d2bd61f5bcc280e6a5c030a36948dd6dd",
-            "x86_64" to "af62d109588db30c8ce11f2027227664520372f8c77259d4155dd92f7cc73a6e",
+            "arm64-v8a" to "189348f277f588ff41f51b4284bf0568bf3d0eab786f6664f8d66847694e0674",
+            "armeabi-v7a" to "9550b32340d790d58fe069b3882815e3d2b20474aa634f01e448392e898cc95b",
+            "x86" to "07967aaad711d16f35f509783f56139903f28f661ebb0415522c10fc0453810f",
+            "x86_64" to "6b2f4129cb1e52d9c9dca5b89d575c7b2a905670da2e0474917d37223f0c8bf5",
         )
     }
 
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/ScrollableWithPivot.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt
index c21cc0c..619f0a2 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt
@@ -20,7 +20,6 @@
 import androidx.compose.foundation.focusGroup
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.Orientation.Horizontal
-import androidx.compose.foundation.gestures.Orientation.Vertical
 import androidx.compose.foundation.gestures.ScrollableState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
@@ -31,12 +30,10 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.ScrollContainerInfo
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
 import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.input.provideScrollContainerInfo
 import androidx.compose.ui.platform.debugInspectorInfo
 
 /* Copied from
@@ -118,17 +115,7 @@
         scrollableNestedScrollConnection(scrollLogic, enabled)
     }
 
-    val scrollContainerInfo = remember(orientation, enabled) {
-        object : ScrollContainerInfo {
-            override fun canScrollHorizontally() = enabled && orientation == Horizontal
-
-            override fun canScrollVertically() = enabled && orientation == Vertical
-        }
-    }
-
-    return this
-        .nestedScroll(nestedScrollConnection, nestedScrollDispatcher.value)
-        .provideScrollContainerInfo(scrollContainerInfo)
+    return this.nestedScroll(nestedScrollConnection, nestedScrollDispatcher.value)
 }
 
 private class ScrollingLogic(
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..46f4ee1 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -29,6 +29,7 @@
     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)
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..bbdb596 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
@@ -103,7 +103,7 @@
         val onBackPressedDispatcher = OnBackPressedDispatcher()
         val dispatcherOwner = object : OnBackPressedDispatcherOwner {
             override fun getLifecycle() = lifecycleOwner.lifecycle
-            override fun getOnBackPressedDispatcher() = onBackPressedDispatcher
+            override val onBackPressedDispatcher = onBackPressedDispatcher
         }
         lateinit var navController: NavHostController
 
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 ecc3af3..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
@@ -13,13 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package androidx.wear.watchface.complications.datasource
 
+import android.support.wearable.complications.ComplicationData as WireComplicationData
 import android.annotation.SuppressLint
 import android.app.Activity
 import android.app.Service
 import android.content.ComponentName
 import android.content.Intent
+import android.os.Build
 import android.os.Handler
 import android.os.IBinder
 import android.os.Looper
@@ -30,6 +33,7 @@
 import androidx.annotation.MainThread
 import androidx.annotation.RestrictTo
 import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationDataExpressionEvaluator
 import androidx.wear.watchface.complications.data.ComplicationType
 import androidx.wear.watchface.complications.data.ComplicationType.Companion.fromWireType
 import androidx.wear.watchface.complications.data.NoDataComplicationData
@@ -37,6 +41,11 @@
 import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.Companion.METADATA_KEY_IMMEDIATE_UPDATE_PERIOD_MILLISECONDS
 import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.ComplicationRequestListener
 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
 
 /**
  * Data associated with complication request in
@@ -191,7 +200,11 @@
  */
 public abstract class ComplicationDataSourceService : Service() {
     private var wrapper: IComplicationProviderWrapper? = null
+    private var lastExpressionEvaluator: ComplicationDataExpressionEvaluator? = null
     internal val mainThreadHandler by lazy { createMainThreadHandler() }
+    internal val mainThreadCoroutineScope by lazy {
+        CoroutineScope(mainThreadHandler.asCoroutineDispatcher())
+    }
 
     /* @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -208,6 +221,11 @@
         return null
     }
 
+    override fun onDestroy() {
+        super.onDestroy()
+        lastExpressionEvaluator?.close()
+    }
+
     /**
      * Called when a complication is activated.
      *
@@ -382,11 +400,7 @@
                                 }
                             }
 
-                            // When no update is needed, the complicationData is going to be null.
-                            iComplicationManager.updateComplicationData(
-                                complicationInstanceId,
-                                complicationData?.asWireComplicationData()
-                            )
+                            complicationData?.asWireComplicationData().evaluateAndUpdateManager()
                         }
 
                         override fun onComplicationDataTimeline(
@@ -442,17 +456,52 @@
                                     }
                                 }
                             }
-                            // When no update is needed, the complicationData is going to be null.
-                            iComplicationManager.updateComplicationData(
-                                complicationInstanceId,
-                                complicationDataTimeline?.asWireComplicationData()
-                            )
+                            complicationDataTimeline?.asWireComplicationData()
+                                .evaluateAndUpdateManager()
+                        }
+
+                        private fun WireComplicationData?.evaluateAndUpdateManager() {
+                            lastExpressionEvaluator?.close() // Cancelling any previous evaluation.
+                            if (
+                            // Will be evaluated by the platform.
+                                Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ||
+                                // When no update is needed, the data is going to be null.
+                                this == null
+                            ) {
+                                iComplicationManager.updateComplicationData(
+                                    complicationInstanceId,
+                                    this
+                                )
+                                return
+                            }
+                            lastExpressionEvaluator =
+                                ComplicationDataExpressionEvaluator(this).apply {
+                                    init()
+                                    listenAndUpdateManager(
+                                        iComplicationManager,
+                                        complicationInstanceId,
+                                    )
+                                }
                         }
                     }
                 )
             }
         }
 
+        private fun ComplicationDataExpressionEvaluator.listenAndUpdateManager(
+            iComplicationManager: IComplicationManager,
+            complicationInstanceId: Int,
+        ) {
+            mainThreadCoroutineScope.launch {
+                // Doing one-off evaluation, the service will be re-invoked.
+                iComplicationManager.updateComplicationData(
+                    complicationInstanceId,
+                    data.filterNotNull().first()
+                )
+                close()
+            }
+        }
+
         @SuppressLint("SyntheticAccessor")
         override fun onComplicationDeactivated(complicationInstanceId: Int) {
             mainThreadHandler.post {
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-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.java b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.java
index cc84470..bc848ab 100644
--- a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.java
+++ b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.java
@@ -18,12 +18,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
 
 import android.content.Intent;
+import android.os.Build;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.RemoteException;
@@ -38,9 +37,9 @@
 import androidx.wear.watchface.complications.data.ComplicationType;
 import androidx.wear.watchface.complications.data.LongTextComplicationData;
 import androidx.wear.watchface.complications.data.PlainComplicationText;
-import androidx.wear.watchface.complications.data.TimeRange;
+import androidx.wear.watchface.complications.data.StringExpression;
+import androidx.wear.watchface.complications.data.StringExpressionComplicationText;
 
-import org.jetbrains.annotations.NotNull;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -48,8 +47,9 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.robolectric.annotation.Config;
 import org.robolectric.annotation.internal.DoNotInstrument;
-import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.shadows.ShadowLog;
 
 import java.time.Instant;
 import java.util.ArrayList;
@@ -72,7 +72,7 @@
     private IComplicationManager mRemoteManager;
 
     private final CountDownLatch mUpdateComplicationDataLatch = new CountDownLatch(1);
-    private IComplicationManager.Stub mLocalManager = new IComplicationManager.Stub() {
+    private final IComplicationManager.Stub mLocalManager = new IComplicationManager.Stub() {
         @Override
         public void updateComplicationData(int complicationSlotId,
                 android.support.wearable.complications.ComplicationData data)
@@ -82,12 +82,42 @@
         }
     };
 
-    private IComplicationProvider.Stub mComplicationProvider;
-    private IComplicationProvider.Stub mNoUpdateComplicationProvider;
-    private IComplicationProvider.Stub mWrongComplicationProvider;
-    private IComplicationProvider.Stub mTimelineProvider;
+    private IComplicationProvider.Stub mProvider;
 
-    private ComplicationDataSourceService mTestService = new ComplicationDataSourceService() {
+    /**
+     * Mock implementation of ComplicationDataSourceService.
+     *
+     * <p>Can't use Mockito because it doesn't like partially implemented classes.
+     */
+    private class MockComplicationDataSourceService extends ComplicationDataSourceService {
+        boolean respondWithTimeline = false;
+
+        /**
+         * Will be used to invoke {@link ComplicationRequestListener#onComplicationData} on
+         * {@link #onComplicationRequest}.
+         */
+        @Nullable
+        ComplicationData responseData;
+
+        /**
+         * Will be used to invoke {@link ComplicationRequestListener#onComplicationDataTimeline} on
+         * {@link #onComplicationRequest}, if {@link #respondWithTimeline} is true.
+         */
+        @Nullable
+        ComplicationDataTimeline responseDataTimeline;
+
+        /** Last request provided to {@link #onComplicationRequest}. */
+        @Nullable
+        ComplicationRequest lastRequest;
+
+        /** Will be returned from {@link #getPreviewData}. */
+        @Nullable
+        ComplicationData previewData;
+
+        /** Last type provided to {@link #getPreviewData}. */
+        @Nullable
+        ComplicationType lastPreviewType;
+
         @NonNull
         @Override
         public Handler createMainThreadHandler() {
@@ -95,20 +125,15 @@
         }
 
         @Override
-        public void onComplicationRequest(
-                @NotNull ComplicationRequest request,
+        public void onComplicationRequest(@NonNull ComplicationRequest request,
                 @NonNull ComplicationRequestListener listener) {
+            lastRequest = request;
             try {
-                String response = request.isImmediateResponseRequired()
-                        ? "hello synchronous " + request.getComplicationInstanceId() :
-                        "hello " + request.getComplicationInstanceId();
-
-                listener.onComplicationData(
-                        new LongTextComplicationData.Builder(
-                                new PlainComplicationText.Builder(response).build(),
-                                ComplicationText.EMPTY
-                        ).build()
-                );
+                if (respondWithTimeline) {
+                    listener.onComplicationDataTimeline(responseDataTimeline);
+                } else {
+                    listener.onComplicationData(responseData);
+                }
             } catch (RemoteException e) {
                 Log.e(TAG, "onComplicationRequest failed with error: ", e);
             }
@@ -117,161 +142,21 @@
         @Nullable
         @Override
         public ComplicationData getPreviewData(@NonNull ComplicationType type) {
-            if (type == ComplicationType.PHOTO_IMAGE) {
-                return null;
-            }
-            return new LongTextComplicationData.Builder(
-                    new PlainComplicationText.Builder("hello preview").build(),
-                    ComplicationText.EMPTY
-            ).build();
+            lastPreviewType = type;
+            return previewData;
         }
-    };
+    }
 
-    private ComplicationDataSourceService mTestServiceNotValidTimeRange =
-            new ComplicationDataSourceService() {
-                @Override
-                public void onComplicationRequest(
-                        @NotNull ComplicationRequest request,
-                        @NonNull ComplicationRequestListener listener) {
-                    try {
-                        listener.onComplicationData(
-                                new LongTextComplicationData.Builder(
-                                        new PlainComplicationText.Builder(
-                                                "hello " + request.getComplicationInstanceId()
-                                        ).build(),
-                                        ComplicationText.EMPTY
-                                ).build()
-                        );
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "onComplicationRequest failed with error: ", e);
-                    }
-                }
-
-                @Nullable
-                @Override
-                public ComplicationData getPreviewData(@NonNull ComplicationType type) {
-                    return new LongTextComplicationData.Builder(
-                            new PlainComplicationText.Builder("hello preview").build(),
-                            ComplicationText.EMPTY
-                    ).setValidTimeRange(TimeRange.between(Instant.now(), Instant.now())).build();
-                }
-            };
-
-    private ComplicationDataSourceService mNoUpdateTestService =
-            new ComplicationDataSourceService() {
-                @NonNull
-                @Override
-                public Handler createMainThreadHandler() {
-                    return mPretendMainThreadHandler;
-                }
-
-                @Override
-                public void onComplicationRequest(
-                        @NotNull ComplicationRequest request,
-                        @NonNull ComplicationRequestListener listener) {
-                    try {
-                        // Null means no update required.
-                        listener.onComplicationData(null);
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "onComplicationRequest failed with error: ", e);
-                    }
-                }
-
-                @Nullable
-                @Override
-                public ComplicationData getPreviewData(@NonNull ComplicationType type) {
-                    return new LongTextComplicationData.Builder(
-                            new PlainComplicationText.Builder("hello preview").build(),
-                            ComplicationText.EMPTY
-                    ).build();
-                }
-            };
-
-    private ComplicationDataSourceService mTimelineTestService =
-            new ComplicationDataSourceService() {
-                @NonNull
-                @Override
-                public Handler createMainThreadHandler() {
-                    return mPretendMainThreadHandler;
-                }
-
-                @Override
-                public void onComplicationRequest(
-                        @NotNull ComplicationRequest request,
-                        @NonNull ComplicationRequestListener listener) {
-                    try {
-                        ArrayList<TimelineEntry> timeline = new ArrayList<>();
-                        timeline.add(new TimelineEntry(
-                                        new TimeInterval(
-                                                Instant.ofEpochSecond(1000),
-                                                Instant.ofEpochSecond(4000)
-                                        ),
-                                        new LongTextComplicationData.Builder(
-                                                new PlainComplicationText.Builder(
-                                                        "A").build(),
-                                                ComplicationText.EMPTY
-                                        ).build()
-                                )
-                        );
-
-                        timeline.add(new TimelineEntry(
-                                        new TimeInterval(
-                                                Instant.ofEpochSecond(6000),
-                                                Instant.ofEpochSecond(8000)
-                                        ),
-                                        new LongTextComplicationData.Builder(
-                                                new PlainComplicationText.Builder(
-                                                        "B").build(),
-                                                ComplicationText.EMPTY
-                                        ).build()
-                                )
-                        );
-
-                        listener.onComplicationDataTimeline(
-                                new ComplicationDataTimeline(
-                                        new LongTextComplicationData.Builder(
-                                                new PlainComplicationText.Builder(
-                                                        "default").build(),
-                                                ComplicationText.EMPTY
-                                        ).build(),
-                                        timeline
-                                ));
-                    } catch (RemoteException e) {
-                        Log.e(TAG, "onComplicationRequest failed with error: ", e);
-                    }
-                }
-
-                @Nullable
-                @Override
-                public ComplicationData getPreviewData(@NonNull ComplicationType type) {
-                    return new LongTextComplicationData.Builder(
-                            new PlainComplicationText.Builder("hello preview").build(),
-                            ComplicationText.EMPTY
-                    ).build();
-                }
-            };
+    private final MockComplicationDataSourceService mService =
+            new MockComplicationDataSourceService();
 
     @SuppressWarnings("deprecation") // b/251211092
     @Before
     public void setUp() {
+        ShadowLog.setLoggable("ComplicationData", Log.DEBUG);
         MockitoAnnotations.initMocks(this);
-        mComplicationProvider =
-                (IComplicationProvider.Stub) mTestService.onBind(
-                        new Intent(
-                                ComplicationDataSourceService.ACTION_COMPLICATION_UPDATE_REQUEST));
-
-        mNoUpdateComplicationProvider =
-                (IComplicationProvider.Stub) mNoUpdateTestService.onBind(
-                        new Intent(
-                                ComplicationDataSourceService.ACTION_COMPLICATION_UPDATE_REQUEST));
-
-        mWrongComplicationProvider =
-                (IComplicationProvider.Stub) mTestServiceNotValidTimeRange.onBind(
-                        new Intent(
-                                ComplicationDataSourceService.ACTION_COMPLICATION_UPDATE_REQUEST));
-
-        mTimelineProvider =
-                (IComplicationProvider.Stub) mTimelineTestService.onBind(
+        mProvider =
+                (IComplicationProvider.Stub) mService.onBind(
                         new Intent(
                                 ComplicationDataSourceService.ACTION_COMPLICATION_UPDATE_REQUEST));
 
@@ -280,14 +165,20 @@
     }
 
     @After
-    public void tareDown() {
+    public void tearDown() {
         mPretendMainThread.quitSafely();
     }
 
     @Test
     public void testOnComplicationRequest() throws Exception {
+        mService.responseData =
+                new LongTextComplicationData.Builder(
+                        new PlainComplicationText.Builder("hello").build(),
+                        ComplicationText.EMPTY
+                ).build();
+
         int id = 123;
-        mComplicationProvider.onUpdate(
+        mProvider.onUpdate(
                 id, ComplicationType.LONG_TEXT.toWireComplicationType(), mLocalManager);
         assertThat(mUpdateComplicationDataLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue();
 
@@ -295,13 +186,71 @@
                 ArgumentCaptor.forClass(
                         android.support.wearable.complications.ComplicationData.class);
         verify(mRemoteManager).updateComplicationData(eq(id), data.capture());
-        assertThat(data.getValue().getLongText().getTextAt(null, 0)).isEqualTo(
-                "hello " + id
-        );
+        assertThat(data.getValue().getLongText().getTextAt(null, 0)).isEqualTo("hello");
+    }
+
+    @Test
+    @Config(sdk = Build.VERSION_CODES.TIRAMISU)
+    public void testOnComplicationRequestWithExpression_doesNotEvaluateExpression()
+            throws Exception {
+        mService.responseData =
+                new LongTextComplicationData.Builder(
+                        new StringExpressionComplicationText(
+                                new StringExpression(new byte[]{1, 2})),
+                        ComplicationText.EMPTY)
+                        .build();
+
+        mProvider.onUpdate(
+                /* complicationInstanceId = */ 123,
+                ComplicationType.LONG_TEXT.toWireComplicationType(),
+                mLocalManager);
+
+        assertThat(mUpdateComplicationDataLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue();
+        verify(mRemoteManager).updateComplicationData(
+                eq(123),
+                eq(new LongTextComplicationData.Builder(
+                        new StringExpressionComplicationText(
+                                new StringExpression(new byte[]{1, 2})),
+                        ComplicationText.EMPTY)
+                        .build()
+                        .asWireComplicationData()));
+    }
+
+    @Test
+    @Config(sdk = Build.VERSION_CODES.S)
+    public void testOnComplicationRequestWithExpressionPreT_evaluatesExpression()
+            throws Exception {
+        mService.responseData =
+                new LongTextComplicationData.Builder(
+                        new StringExpressionComplicationText(
+                                new StringExpression(new byte[]{1, 2})),
+                        ComplicationText.EMPTY)
+                        .build();
+
+        mProvider.onUpdate(
+                /* complicationInstanceId = */ 123,
+                ComplicationType.LONG_TEXT.toWireComplicationType(),
+                mLocalManager);
+
+        assertThat(mUpdateComplicationDataLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue();
+        verify(mRemoteManager).updateComplicationData(
+                eq(123),
+                eq(new LongTextComplicationData.Builder(
+                        // TODO(b/260065006): Verify that it is actually evaluated.
+                        new StringExpressionComplicationText(
+                                new StringExpression(new byte[]{1, 2})),
+                        ComplicationText.EMPTY)
+                        .build()
+                        .asWireComplicationData()));
     }
 
     @Test
     public void testOnComplicationRequestWrongType() throws Exception {
+        mService.responseData =
+                new LongTextComplicationData.Builder(
+                        new PlainComplicationText.Builder("hello").build(),
+                        ComplicationText.EMPTY
+                ).build();
         int id = 123;
         AtomicReference<Throwable> exception = new AtomicReference<>();
         CountDownLatch exceptionLatch = new CountDownLatch(1);
@@ -311,7 +260,7 @@
             exceptionLatch.countDown();
         });
 
-        mComplicationProvider.onUpdate(
+        mProvider.onUpdate(
                 id, ComplicationType.SHORT_TEXT.toWireComplicationType(), mLocalManager);
 
         assertThat(exceptionLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue();
@@ -319,17 +268,11 @@
     }
 
     @Test
-    public void testOnComplicationRequestWrongValidTimeRange() throws Exception {
-        int id = 123;
-        mWrongComplicationProvider.onUpdate(
-                id, ComplicationType.SHORT_TEXT.toWireComplicationType(), mLocalManager);
-        assertThrows(IllegalArgumentException.class, ShadowLooper::runUiThreadTasks);
-    }
-
-    @Test
     public void testOnComplicationRequestNoUpdateRequired() throws Exception {
+        mService.responseData = null;
+
         int id = 123;
-        mNoUpdateComplicationProvider.onUpdate(
+        mProvider.onUpdate(
                 id, ComplicationType.LONG_TEXT.toWireComplicationType(), mLocalManager);
         assertThat(mUpdateComplicationDataLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue();
 
@@ -342,23 +285,55 @@
 
     @Test
     public void testGetComplicationPreviewData() throws Exception {
-        assertThat(mComplicationProvider.getComplicationPreviewData(
+        mService.previewData = new LongTextComplicationData.Builder(
+                new PlainComplicationText.Builder("hello preview").build(),
+                ComplicationText.EMPTY
+        ).build();
+
+        assertThat(mProvider.getComplicationPreviewData(
                 ComplicationType.LONG_TEXT.toWireComplicationType()
         ).getLongText().getTextAt(null, 0)).isEqualTo("hello preview");
     }
 
     @Test
-    public void testGetComplicationPreviewDataReturnsNull() throws Exception {
-        // The ComplicationProvider doesn't support PHOTO_IMAGE so null should be returned.
-        assertNull(mComplicationProvider.getComplicationPreviewData(
-                ComplicationType.PHOTO_IMAGE.toWireComplicationType())
-        );
-    }
-
-    @Test
     public void testTimelineTestService() throws Exception {
+        mService.respondWithTimeline = true;
+        ArrayList<TimelineEntry> timeline = new ArrayList<>();
+        timeline.add(new TimelineEntry(
+                        new TimeInterval(
+                                Instant.ofEpochSecond(1000),
+                                Instant.ofEpochSecond(4000)
+                        ),
+                        new LongTextComplicationData.Builder(
+                                new PlainComplicationText.Builder(
+                                        "A").build(),
+                                ComplicationText.EMPTY
+                        ).build()
+                )
+        );
+        timeline.add(new TimelineEntry(
+                        new TimeInterval(
+                                Instant.ofEpochSecond(6000),
+                                Instant.ofEpochSecond(8000)
+                        ),
+                        new LongTextComplicationData.Builder(
+                                new PlainComplicationText.Builder(
+                                        "B").build(),
+                                ComplicationText.EMPTY
+                        ).build()
+                )
+        );
+        mService.responseDataTimeline = new ComplicationDataTimeline(
+                new LongTextComplicationData.Builder(
+                        new PlainComplicationText.Builder(
+                                "default").build(),
+                        ComplicationText.EMPTY
+                ).build(),
+                timeline
+        );
+
         int id = 123;
-        mTimelineProvider.onUpdate(
+        mProvider.onUpdate(
                 id, ComplicationType.LONG_TEXT.toWireComplicationType(), mLocalManager);
         assertThat(mUpdateComplicationDataLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue();
 
@@ -389,6 +364,11 @@
     @Test
     public void testImmediateRequest() throws Exception {
         int id = 123;
+        mService.responseData =
+                new LongTextComplicationData.Builder(
+                        new PlainComplicationText.Builder("hello").build(),
+                        ComplicationText.EMPTY
+                ).build();
         HandlerThread thread = new HandlerThread("testThread");
 
         try {
@@ -400,7 +380,8 @@
 
             threadHandler.post(() -> {
                         try {
-                            response.set(mComplicationProvider.onSynchronousComplicationRequest(123,
+                            response.set(mProvider.onSynchronousComplicationRequest(
+                                    123,
                                     ComplicationType.LONG_TEXT.toWireComplicationType()));
                             doneLatch.countDown();
                         } catch (RemoteException e) {
@@ -410,9 +391,7 @@
             );
 
             assertThat(doneLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue();
-            assertThat(response.get().getLongText().getTextAt(null, 0)).isEqualTo(
-                    "hello synchronous " + id
-            );
+            assertThat(response.get().getLongText().getTextAt(null, 0)).isEqualTo("hello");
         } finally {
             thread.quitSafely();
         }
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 17770c2..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,13 +16,16 @@
 
 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
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.launch
 
@@ -39,13 +42,13 @@
     val unevaluatedData: WireComplicationData,
 ) : AutoCloseable {
 
-    private val _data: MutableStateFlow<WireComplicationData?> = MutableStateFlow(null)
+    private val _data = MutableStateFlow<WireComplicationData?>(null)
 
     /**
      * The evaluated data, or `null` if it wasn't evaluated yet, or [NoDataComplicationData] if it
      * wasn't possible to evaluate the [unevaluatedData].
      */
-    val data = _data.asStateFlow()
+    val data: StateFlow<WireComplicationData?> = _data.asStateFlow()
 
     @GuardedBy("listeners")
     private val listeners = mutableMapOf<Listener, CoroutineScope>()
@@ -64,7 +67,7 @@
             listeners[listener] = CoroutineScope(executor.asCoroutineDispatcher()).apply {
                 launch {
                     data.collect {
-                        if (it != null) listener(it)
+                        if (it != null) listener.accept(it)
                     }
                 }
             }
@@ -89,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/build.gradle b/wear/watchface/watchface-style/build.gradle
index 8f3b413..8bb7927 100644
--- a/wear/watchface/watchface-style/build.gradle
+++ b/wear/watchface/watchface-style/build.gradle
@@ -23,6 +23,34 @@
     id("kotlin-android")
 }
 
+// This task copies the apks provided by the `apkAssets` configuration and places them in the
+// assets folder. This allows a build time generation of the sample apps.
+def copyApkTaskProvider = tasks.register("copyApkAssets", Copy) {
+    description = "Copies the asset apks provided by testfixture projects"
+    dependsOn(configurations.getByName("apkAssets"))
+    from(configurations.getByName("apkAssets").incoming.artifactView {}.files)
+    into(layout.buildDirectory.dir("intermediates/apkAssets"))
+
+    // Note that the artifact directory included contain multiple output-metadata.json files built
+    // with the apks. Since we're not interested in those we can simply exclude duplicates.
+    duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
+}
+
+// Define a configuration that can be resolved. This project is the consumer of test apks, i.e. it
+// contains the integration tests.
+configurations {
+    apkAssets {
+        canBeConsumed = false
+        canBeResolved = true
+        attributes {
+            attribute(
+                    LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
+                    objects.named(LibraryElements, 'apkAssets')
+            )
+        }
+    }
+}
+
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
     api(project(":wear:watchface:watchface-complications"))
@@ -45,6 +73,14 @@
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.testRules)
     androidTestImplementation(libs.truth)
+    androidTestApi(project(":wear:watchface:watchface-style-old-api-test-stub"))
+
+    apkAssets(project(":wear:watchface:watchface-style-old-api-test-service"))
+}
+
+// It makes sure that the apks are generated before the assets are packed.
+afterEvaluate {
+    tasks.named("generateDebugAndroidTestAssets").configure { it.dependsOn(copyApkTaskProvider) }
 }
 
 android {
@@ -54,6 +90,7 @@
 
     // Use Robolectric 4.+
     testOptions.unitTests.includeAndroidResources = true
+    sourceSets.androidTest.assets.srcDir(copyApkTaskProvider)
     namespace "androidx.wear.watchface.style"
 }
 
diff --git a/wear/watchface/watchface-style/old-api-test-service/build.gradle b/wear/watchface/watchface-style/old-api-test-service/build.gradle
new file mode 100644
index 0000000..6d668ff
--- /dev/null
+++ b/wear/watchface/watchface-style/old-api-test-service/build.gradle
@@ -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.
+ */
+
+import androidx.build.LibraryType
+import com.android.build.api.artifact.SingleArtifact
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+    id("kotlin-android")
+}
+
+dependencies {
+    compileOnly(project(":annotation:annotation-sampled"))
+
+    api(project(":wear:watchface:watchface-style-old-api-test-stub"))
+
+    implementation("androidx.core:core:1.1.0")
+    implementation("androidx.wear.watchface:watchface-style:1.0.0")
+    implementation("androidx.wear.watchface:watchface-data:1.0.0")
+    api(libs.kotlinStdlib)
+}
+
+androidx {
+    name = "AndroidX WatchFace Style Old Api Test Service"
+    type = LibraryType.SAMPLES
+    inceptionYear = "2022"
+    description = "Test service built with v1.0.0 of the API, used to check for binary AIDL compat"
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 26
+    }
+    buildTypes {
+        release {
+            minifyEnabled true
+            shrinkResources true
+            proguardFiles getDefaultProguardFile('proguard-android.txt')
+        }
+    }
+    buildFeatures {
+        aidl = true
+    }
+    namespace "androidx.wear.watchface.style.test.oldApiTestService"
+}
+
+/*
+ * Allow integration tests to consume the APK produced by this project
+ */
+configurations {
+    apkAssets {
+        canBeConsumed = true
+        canBeResolved = false
+        attributes {
+            attribute(
+                    LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
+                    objects.named(LibraryElements, 'apkAssets')
+            )
+        }
+    }
+}
+
+androidComponents {
+    onVariants(selector().all().withBuildType("release"), { variant ->
+        artifacts {
+            apkAssets(variant.artifacts.get(SingleArtifact.APK.INSTANCE))
+        }
+    })
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-style/old-api-test-service/src/main/AndroidManifest.xml b/wear/watchface/watchface-style/old-api-test-service/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9d39564
--- /dev/null
+++ b/wear/watchface/watchface-style/old-api-test-service/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?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">
+    <application android:requestLegacyExternalStorage="true">
+        <service android:name="androidx.wear.watchface.style.test.StyleEchoService"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="com.google.android.wearable.action.TEST"/>
+            </intent-filter>
+        </service>
+    </application>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+</manifest>
diff --git a/wear/watchface/watchface-style/old-api-test-service/src/main/java/androidx/wear/watchface/style/test/StyleEchoService.kt b/wear/watchface/watchface-style/old-api-test-service/src/main/java/androidx/wear/watchface/style/test/StyleEchoService.kt
new file mode 100644
index 0000000..a38577b
--- /dev/null
+++ b/wear/watchface/watchface-style/old-api-test-service/src/main/java/androidx/wear/watchface/style/test/StyleEchoService.kt
@@ -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.
+ */
+
+package androidx.wear.watchface.style.test
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+import android.util.Log
+import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat
+import androidx.wear.watchface.style.UserStyleSchema
+
+public class StyleEchoService : Service() {
+    override fun onBind(intent: Intent?): IBinder = IStyleEchoServiceStub()
+}
+
+public open class IStyleEchoServiceStub : IStyleEchoService.Stub() {
+    override fun roundTripToApiUserStyleSchemaWireFormat(
+        styleSchema: UserStyleSchemaWireFormat
+    ): UserStyleSchemaWireFormat {
+        val apiUserStyleSchema = UserStyleSchema(styleSchema)
+        Log.i(TAG, "Received $apiUserStyleSchema")
+        return apiUserStyleSchema.toWireFormat()
+    }
+
+    companion object {
+        const val TAG = "IStyleEchoServiceStub"
+    }
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-style/old-api-test-stub/build.gradle b/wear/watchface/watchface-style/old-api-test-stub/build.gradle
new file mode 100644
index 0000000..ba36ffc
--- /dev/null
+++ b/wear/watchface/watchface-style/old-api-test-stub/build.gradle
@@ -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.
+ */
+
+import androidx.build.LibraryType
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("kotlin-android")
+}
+
+dependencies {
+    compileOnly(project(":annotation:annotation-sampled"))
+
+    implementation("androidx.core:core:1.1.0")
+    implementation("androidx.wear.watchface:watchface-data:1.0.0")
+    api(libs.kotlinStdlib)
+}
+
+androidx {
+    name = "AndroidX WatchFace Style Old Api Test Stub"
+    type = LibraryType.INTERNAL_TEST_LIBRARY
+    inceptionYear = "2022"
+    description = "Test stub built with v1.0.0 of the API, used to check for binary AIDL compat"
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 26
+    }
+    buildFeatures {
+        aidl = true
+    }
+    namespace "androidx.wear.watchface.style.test.oldApiTestStub"
+}
diff --git a/wear/watchface/watchface-style/old-api-test-stub/src/main/AndroidManifest.xml b/wear/watchface/watchface-style/old-api-test-stub/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9a20520
--- /dev/null
+++ b/wear/watchface/watchface-style/old-api-test-stub/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?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"/>
diff --git a/wear/watchface/watchface-style/old-api-test-stub/src/main/aidl/androidx/wear/watchface/style/test/IStyleEchoService.aidl b/wear/watchface/watchface-style/old-api-test-stub/src/main/aidl/androidx/wear/watchface/style/test/IStyleEchoService.aidl
new file mode 100644
index 0000000..51fefd7
--- /dev/null
+++ b/wear/watchface/watchface-style/old-api-test-stub/src/main/aidl/androidx/wear/watchface/style/test/IStyleEchoService.aidl
@@ -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.
+ */
+
+package androidx.wear.watchface.style.test;
+
+import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat;
+
+/**
+ * Interface of a service that allows testing of IPC round trips vs an old binary.
+ *
+ * @hide
+ */
+interface IStyleEchoService {
+    /**
+     * Returns the UserStyleSchemaWireFormat after converting to and from API format.
+     */
+    UserStyleSchemaWireFormat roundTripToApiUserStyleSchemaWireFormat(
+        in UserStyleSchemaWireFormat styleSchema) = 0;
+}
diff --git a/wear/watchface/watchface-style/src/androidTest/AndroidManifest.xml b/wear/watchface/watchface-style/src/androidTest/AndroidManifest.xml
index 91eee54..872539b 100644
--- a/wear/watchface/watchface-style/src/androidTest/AndroidManifest.xml
+++ b/wear/watchface/watchface-style/src/androidTest/AndroidManifest.xml
@@ -14,4 +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" >
+    <queries>
+        <package android:name="androidx.wear.watchface.style.test.oldApiTestService"/>
+    </queries>
+</manifest>
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
new file mode 100644
index 0000000..5cf9133
--- /dev/null
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/OldClientAidlCompatTest.kt
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.watchface.style
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.graphics.drawable.Icon
+import android.os.IBinder
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.rule.ServiceTestRule
+import androidx.wear.watchface.style.test.IStyleEchoService
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption
+import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
+import androidx.wear.watchface.style.test.R
+import com.google.common.truth.Truth.assertThat
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@LargeTest
+class OldClientAidlCompatTest {
+    @get:Rule
+    val serviceRule = ServiceTestRule()
+
+    companion object {
+        private val CONTEXT: Context = ApplicationProvider.getApplicationContext()
+
+        private val COLOR_STYLE_SETTING = ListUserStyleSetting(
+            UserStyleSetting.Id("COLOR_STYLE_SETTING"),
+            CONTEXT.resources,
+            R.string.colors_style_setting,
+            R.string.colors_style_setting_description,
+            icon = null,
+            options = listOf(
+                ListUserStyleSetting.ListOption(
+                    Option.Id("RED_STYLE"),
+                    CONTEXT.resources,
+                    R.string.colors_style_red,
+                    R.string.colors_style_red_screen_reader,
+                    Icon.createWithResource(CONTEXT, R.drawable.red_style)
+                ),
+                ListUserStyleSetting.ListOption(
+                    Option.Id("GREEN_STYLE"),
+                    CONTEXT.resources,
+                    R.string.colors_style_green,
+                    R.string.colors_style_green_screen_reader,
+                    Icon.createWithResource(CONTEXT, R.drawable.green_style)
+                ),
+                ListUserStyleSetting.ListOption(
+                    Option.Id("BLUE_STYLE"),
+                    CONTEXT.resources,
+                    R.string.colors_style_blue,
+                    R.string.colors_style_blue_screen_reader,
+                    Icon.createWithResource(CONTEXT, R.drawable.blue_style)
+                )
+            ),
+            listOf(
+                WatchFaceLayer.BASE,
+                WatchFaceLayer.COMPLICATIONS,
+                WatchFaceLayer.COMPLICATIONS_OVERLAY
+            )
+        )
+
+        private val DRAW_HOUR_PIPS_SETTING = UserStyleSetting.BooleanUserStyleSetting(
+            UserStyleSetting.Id("DRAW_HOUR_PIPS_STYLE_SETTING"),
+            CONTEXT.resources,
+            R.string.watchface_pips_setting,
+            R.string.watchface_pips_setting_description,
+            icon = null,
+            listOf(WatchFaceLayer.BASE),
+            true
+        )
+
+        private val WATCH_HAND_LENGTH_SETTING = UserStyleSetting.DoubleRangeUserStyleSetting(
+            UserStyleSetting.Id("WATCH_HAND_LENGTH_STYLE_SETTING"),
+            CONTEXT.resources,
+            R.string.watchface_hand_length_setting,
+            R.string.watchface_hand_length_setting_description,
+            icon = null,
+            minimumValue = 0.25,
+            maximumValue = 1.0,
+            listOf(WatchFaceLayer.COMPLICATIONS_OVERLAY),
+            defaultValue = 0.75
+        )
+
+        @Suppress("Deprecation")
+        private val COMPLICATIONS_STYLE_SETTING = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("COMPLICATIONS_STYLE_SETTING"),
+            CONTEXT.resources,
+            R.string.watchface_complications_setting,
+            R.string.watchface_complications_setting_description,
+            icon = null,
+            complicationConfig = listOf(
+                ComplicationSlotsOption(
+                    Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
+                    CONTEXT.resources,
+                    R.string.watchface_complications_setting_both,
+                    icon = null,
+                    // NB this list is empty because each [ComplicationSlotOverlay] is applied on
+                    // top of the initial config.
+                    listOf()
+                ),
+                ComplicationSlotsOption(
+                    Option.Id("NO_COMPLICATIONS"),
+                    CONTEXT.resources,
+                    R.string.watchface_complications_setting_none,
+                    icon = null,
+                    listOf(
+                        ComplicationSlotOverlay(complicationSlotId = 1, enabled = false),
+                        ComplicationSlotOverlay(complicationSlotId = 2, enabled = false)
+                    )
+                ),
+                ComplicationSlotsOption(
+                    Option.Id("LEFT_COMPLICATION"),
+                    CONTEXT.resources,
+                    R.string.watchface_complications_setting_left,
+                    icon = null,
+                    listOf(ComplicationSlotOverlay(complicationSlotId = 1, enabled = false))
+                ),
+                ComplicationSlotsOption(
+                    Option.Id("RIGHT_COMPLICATION"),
+                    CONTEXT.resources,
+                    R.string.watchface_complications_setting_right,
+                    icon = null,
+                    listOf(ComplicationSlotOverlay(complicationSlotId = 2, enabled = false))
+                )
+            ),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+
+        private val LONG_RANGE_SETTING = UserStyleSetting.LongRangeUserStyleSetting(
+            UserStyleSetting.Id("HOURS_DRAW_FREQ_STYLE_SETTING"),
+            CONTEXT.resources,
+            R.string.watchface_draw_hours_freq_setting,
+            R.string.watchface_draw_hours_freq_setting_description,
+            icon = null,
+            minimumValue = 0,
+            maximumValue = 4,
+            listOf(WatchFaceLayer.BASE),
+            defaultValue = 1
+        )
+
+        private val SCHEMA = UserStyleSchema(
+            listOf(
+                COLOR_STYLE_SETTING,
+                DRAW_HOUR_PIPS_SETTING,
+                WATCH_HAND_LENGTH_SETTING,
+                COMPLICATIONS_STYLE_SETTING,
+                LONG_RANGE_SETTING
+            )
+        )
+
+        private const val ACTON = "com.google.android.wearable.action.TEST"
+        private const val TEXT_FIXTURE_APK = "watchface-style-old-api-test-service-release.apk"
+        private const val PACKAGE_NAME = "androidx.wear.watchface.style.test.oldApiTestService"
+    }
+
+    @Before
+    fun setUp() = withPackageName(PACKAGE_NAME) {
+        install(TEXT_FIXTURE_APK)
+    }
+
+    @After
+    fun tearDown() = withPackageName(PACKAGE_NAME) {
+        uninstall()
+    }
+
+    @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) })
+        )
+
+        val result = UserStyleSchema(
+            service.roundTripToApiUserStyleSchemaWireFormat(SCHEMA.toWireFormat())
+        )
+
+        // We expect five root settings back. Some of the details will have been clipped which
+        // is expected because not all the current features are supported by v1.1.1, the main
+        // thing is the service didn't crash!
+        assertThat(result.rootUserStyleSettings.size).isEqualTo(5)
+    }
+
+    private suspend fun bindService(intent: Intent): IBinder = suspendCoroutine { continuation ->
+        val bound = CONTEXT.bindService(
+            intent,
+            object : ServiceConnection {
+                override fun onServiceConnected(componentName: ComponentName, binder: IBinder) {
+                    continuation.resume(binder)
+                }
+
+                override fun onServiceDisconnected(p0: ComponentName?) {
+                }
+            },
+            Context.BIND_AUTO_CREATE or Context.BIND_IMPORTANT
+        )
+
+        if (!bound) {
+            continuation.resumeWithException(
+                IllegalStateException("No service found for $intent.")
+            )
+        }
+    }
+}
diff --git a/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/TestHelper.kt b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/TestHelper.kt
new file mode 100644
index 0000000..2f90085
--- /dev/null
+++ b/wear/watchface/watchface-style/src/androidTest/java/androidx/wear/watchface/style/TestHelper.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.watchface.style
+
+import android.os.Build
+import android.os.Environment
+import android.os.ParcelFileDescriptor
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth
+import java.io.File
+
+// Test helpers forked from http://aosp/2218950
+
+fun withPackageName(packageName: String, block: WithPackageBlock.() -> Unit) {
+    block(WithPackageBlock(packageName))
+}
+
+class WithPackageBlock internal constructor(private val packageName: String) {
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val uiAutomation = instrumentation.uiAutomation
+
+    // `externalMediaDirs` is deprecated. Docs encourage to put media in MediaStore instead. Here
+    // we just need a folder that can be accessed by both app and shell user so should be fine.
+    @Suppress("deprecation")
+    private val dirUsableByAppAndShell by lazy {
+        when {
+            Build.VERSION.SDK_INT >= 29 -> {
+                // On Android Q+ we are using the media directory because that is
+                // the directory that the shell has access to. Context: b/181601156
+                // This is the same logic of Outputs#init to determine `dirUsableByAppAndShell`.
+                InstrumentationRegistry.getInstrumentation().context.externalMediaDirs.firstOrNull {
+                    Environment.getExternalStorageState(it) == Environment.MEDIA_MOUNTED
+                }
+            }
+            Build.VERSION.SDK_INT <= 22 -> {
+                // prior to API 23, shell didn't have access to externalCacheDir
+                InstrumentationRegistry.getInstrumentation().context.cacheDir
+            }
+            else -> InstrumentationRegistry.getInstrumentation().context.externalCacheDir
+        } ?: throw IllegalStateException("Unable to select a directory for writing files.")
+    }
+
+    public fun uninstall() = executeCommand("pm uninstall $packageName")
+
+    public fun install(apkName: String) {
+        // Contains all the clean up actions to perform at the end of the execution
+        val cleanUpBlocks = mutableListOf<() -> (Unit)>()
+
+        try {
+            // First writes in a temp file the apk from the assets
+            val tmpApkFile = File(dirUsableByAppAndShell, "tmp_$apkName").also { file ->
+                file.delete()
+                file.createNewFile()
+                file.deleteOnExit()
+                file.outputStream().use { instrumentation.context.assets.open(apkName).copyTo(it) }
+            }
+            cleanUpBlocks.add { tmpApkFile.delete() }
+
+            // Then moves it to a destination that can be used to install it
+            val destApkPath = "$TEMP_DIR/$apkName"
+            Truth.assertThat(executeCommand("mv ${tmpApkFile.absolutePath} $destApkPath")).isEmpty()
+            cleanUpBlocks.add { executeCommand("rm $destApkPath") }
+
+            // This mimes the behaviour of `adb install-multiple` using an install session.
+            // For reference:
+            // https://source.corp.google.com/android-internal/packages/modules/adb/client/adb_install.cpp
+
+            // Creates an install session
+            val installCreateOutput = executeCommand("pm install-create -t").first().trim()
+            val sessionId = REGEX_SESSION_ID
+                .find(installCreateOutput)!!
+                .groups[1]!!
+                .value
+                .toLong()
+
+            // Adds the base apk to the install session
+            val successBaseApk =
+                executeCommand("pm install-write $sessionId base.apk $TEMP_DIR/$apkName")
+                    .first()
+                    .trim()
+                    .startsWith("Success")
+            if (!successBaseApk) {
+                throw IllegalStateException("Could not add $apkName to install session $sessionId")
+            }
+
+            // Commit the install transaction. Note that install-commit may not print any output
+            // if it fails.
+            val commitCommandOutput = executeCommand("pm install-commit $sessionId")
+            val firstLine = commitCommandOutput.firstOrNull()?.trim()
+            if (firstLine == null || firstLine != "Success") {
+                throw IllegalStateException(
+                    "pm install-commit failed: ${commitCommandOutput.joinToString("\n")}"
+                )
+            }
+        } finally {
+            // Runs all the clean up blocks. This will clean up also partial operations in case
+            // there is an issue during install
+            cleanUpBlocks.forEach { it() }
+        }
+    }
+
+    private fun executeCommand(command: String): List<String> {
+        Log.d(TAG, "Executing command: `$command`")
+        return ParcelFileDescriptor.AutoCloseInputStream(uiAutomation.executeShellCommand(command))
+            .bufferedReader()
+            .lineSequence()
+            .toList()
+    }
+
+    companion object {
+        private const val TAG = "TestManager"
+        private const val TEMP_DIR = "/data/local/tmp/"
+        private val REGEX_SESSION_ID = """\[(\d+)\]""".toRegex()
+    }
+}
\ No newline at end of file
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 8702747..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
@@ -162,6 +162,36 @@
     }
 
     @Test
+    @Suppress("deprecation")
+    public fun listUserStyleSettingWireFormatRoundTrip_noScreenReaderName() {
+        val listUserStyleSetting = ListUserStyleSetting(
+            UserStyleSetting.Id("list"),
+            context.resources,
+            R.string.colors_style_setting,
+            R.string.colors_style_setting_description,
+            icon = null,
+            options = listOf(
+                ListOption(
+                    UserStyleSetting.Option.Id("one"),
+                    context.resources,
+                    R.string.ith_option,
+                    icon = null
+                )
+            ),
+            listOf(WatchFaceLayer.BASE, WatchFaceLayer.COMPLICATIONS_OVERLAY)
+        )
+
+        val listUserStyleSettingAfterRoundTrip = ListUserStyleSetting(
+            listUserStyleSetting.toWireFormat()
+        )
+
+        val option0 = listUserStyleSettingAfterRoundTrip.options[0] as ListOption
+        Truth.assertThat(option0.displayName).isEqualTo("1st option")
+        // We expect screenReaderName to be back filled by the displayName.
+        Truth.assertThat(option0.screenReaderName).isEqualTo("1st option")
+    }
+
+    @Test
     public fun complicationSlotsOptionsWithIndices() {
         val complicationSetting = ComplicationSlotsUserStyleSetting(
             UserStyleSetting.Id("complications_style_setting1"),
@@ -209,4 +239,85 @@
         Truth.assertThat(option2.displayName).isEqualTo("3rd option")
         Truth.assertThat(option2.screenReaderName).isEqualTo("3rd list option")
     }
+
+    @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() {
+        val complicationSetting = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting1"),
+            displayName = "Complications",
+            description = "Number and position",
+            icon = null,
+            complicationConfig = listOf(
+                ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id("one"),
+                    context.resources,
+                    displayNameResourceId = R.string.ith_option,
+                    icon = null,
+                    emptyList()
+                )
+            ),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+
+        val complicationSettingAfterRoundTrip = ComplicationSlotsUserStyleSetting(
+            complicationSetting.toWireFormat()
+        )
+
+        val option0 = complicationSettingAfterRoundTrip.options[0] as ComplicationSlotsOption
+        Truth.assertThat(option0.displayName).isEqualTo("1st option")
+        // We expect screenReaderName to be back filled by the displayName.
+        Truth.assertThat(option0.screenReaderName).isEqualTo("1st option")
+    }
 }
\ No newline at end of file
diff --git a/wear/watchface/watchface-style/src/androidTest/res/drawable-nodpi/blue_style.png b/wear/watchface/watchface-style/src/androidTest/res/drawable-nodpi/blue_style.png
new file mode 100644
index 0000000..2a8e5f0
--- /dev/null
+++ b/wear/watchface/watchface-style/src/androidTest/res/drawable-nodpi/blue_style.png
Binary files differ
diff --git a/wear/watchface/watchface-style/src/androidTest/res/drawable-nodpi/green_style.png b/wear/watchface/watchface-style/src/androidTest/res/drawable-nodpi/green_style.png
new file mode 100644
index 0000000..69166e3
--- /dev/null
+++ b/wear/watchface/watchface-style/src/androidTest/res/drawable-nodpi/green_style.png
Binary files differ
diff --git a/wear/watchface/watchface-style/src/androidTest/res/drawable-nodpi/red_style.png b/wear/watchface/watchface-style/src/androidTest/res/drawable-nodpi/red_style.png
new file mode 100644
index 0000000..a328129
--- /dev/null
+++ b/wear/watchface/watchface-style/src/androidTest/res/drawable-nodpi/red_style.png
Binary files differ
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 1510cb7..aa37205 100644
--- a/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
+++ b/wear/watchface/watchface-style/src/androidTest/res/values/strings.xml
@@ -35,4 +35,231 @@
 
     <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>
+
+    <!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
+    <string name="colors_style_green">Green</string>
+
+    <!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
+    <string name="colors_style_blue">Blue</string>
+
+    <!-- An option within the watch face color style theme settings -->
+    <string name="colors_style_red_screen_reader">Red color theme</string>
+
+    <!-- An option within the watch face color style theme settings -->
+    <string name="colors_style_green_screen_reader">Green color theme</string>
+
+    <!-- An option within the watch face color style theme settings -->
+    <string name="colors_style_blue_screen_reader">Blue color theme</string>
+
+    <!-- Name of watchface style category for selecting the hand style [CHAR LIMIT=20] -->
+    <string name="hand_style_setting" translatable="false">Hand Style</string>
+
+    <!-- Subtitle for the menu option to select the watch face hand style [CHAR LIMIT=20] -->
+    <string name="hand_style_setting_description" translatable="false">Hand visual look</string>
+
+    <!-- An option with the watch face hand style settings [CHAR LIMIT=20] -->
+    <string name="hand_style_classic" translatable="false">Classic</string>
+
+    <!-- An option with the watch face hand style settings  for use by screen readers -->
+    <string name="hand_style_classic_screen_reader" translatable="false">Classic watch hand
+    style</string>
+
+    <!-- An option with the watch face hand style settings [CHAR LIMIT=20] -->
+    <string name="hand_style_modern" translatable="false">Modern</string>
+
+    <!-- An option with the watch face hand style settings  for use by screen readers -->
+    <string name="hand_style_modern_screen_reader" translatable="false">Modern watch hand
+    style</string>
+
+    <!-- An option with the watch face hand style settings [CHAR LIMIT=20] -->
+    <string name="hand_style_gothic" translatable="false">Gothic</string>
+
+    <!-- An option with the watch face hand style settings for use by screen readers -->
+    <string name="hand_style_gothic_screen_reader" translatable="false">Gothic watch hand
+    style</string>
+
+    <!-- An option within the analog watch face to draw pips to mark each hour [CHAR LIMIT=20] -->
+    <string name="watchface_pips_setting">Hour Pips</string>
+
+    <!-- Subtitle for the menu option to configure if we should draw pips to mark each hour
+     on the watch face [CHAR LIMIT=20] -->
+    <string name="watchface_pips_setting_description">Whether to draw or not</string>
+
+    <!-- A menu option to select a widget that lets us configure the length of the watch hands
+    [CHAR LIMIT=20] -->
+    <string name="watchface_hand_length_setting">Hand length</string>
+
+    <!-- Sub title for the menu option to select a widget that lets us configure the length of the
+    watch hands [CHAR LIMIT=20] -->
+    <string name="watchface_hand_length_setting_description">Scale of watch hands</string>
+
+    <!-- A menu option to select a widget that lets us configure the Complications (a watch
+    making term) [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting">Complications</string>
+
+    <!-- Sub title for the menu option to select a widget that lets us configure the Complications
+    (a watch making term) [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting_description">Number and position</string>
+
+    <!-- Menu option within the complications configuration widget that lets us select both
+    the left and the right complication for rendering. [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting_both">Both</string>
+
+    <!-- Menu option within the complications configuration widget that lets us select no
+    complications for rendering. [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting_none">None</string>
+
+    <!-- Menu option within the complications configuration widget that lets us select only the left
+    complication for rendering. [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting_left">Left</string>
+
+    <!-- Menu option within the complications configuration widget that lets us select only the
+    right complication for rendering. [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting_right">Right</string>
+
+    <!-- Menu option to select a widget that lets us configure the frequency of hours labeling
+    [CHAR LIMIT=20] -->
+    <string name="watchface_draw_hours_freq_setting">Hours labeling</string>
+
+    <!-- Sub title for the menu option to select a widget that lets us configure the frequency of
+    hours labeling [CHAR LIMIT=20] -->
+    <string name="watchface_draw_hours_freq_setting_description">Labeling frequency</string>
+
+    <!-- Menu option to select a widget that lets us configure whether to show hour pips
+    [CHAR LIMIT=20] -->
+    <string name="watchface_draw_hours_setting">Hour pips</string>
+
+    <!-- Sub title for the menu option to select a widget that lets us configure whether to show
+    hour pips [CHAR LIMIT=20] -->
+    <string name="watchface_draw_hours_setting_description">Display on/off</string>
+
+    <!-- Name of the left complication for use visually in the companion editor. [CHAR LIMIT=20] -->
+    <string name="left_complication_screen_name">Left</string>
+
+    <!-- Name of the left complication for use by in the companion editor screen reader. -->
+    <string name="left_complication_screen_reader_name">Left complication.</string>
+
+    <!-- Name of the right complication for use visually in the companion editor.
+    [CHAR LIMIT=20] -->
+    <string name="right_complication_screen_name">Right</string>
+
+    <!-- Name of the right complication for use by the companion editor screen reader. -->
+    <string name="right_complication_screen_reader_name">Right complication.</string>
+
+    <!-- Name of a complication at the top of the screen, for use visually in the companion editor.
+    [CHAR LIMIT=20] -->
+    <string name="upper_complication_screen_name">Upper</string>
+
+    <!-- Name of a complication at the top of the screen, for use by the companion editor screen
+    reader. -->
+    <string name="upper_complication_screen_reader_name">Upper complication.</string>
+
+    <!-- Name of a complication at the bottom of the screen, for use visually in the companion
+    editor. [CHAR LIMIT=20] -->
+    <string name="lower_complication_screen_name">Lower</string>
+
+    <!-- Name of a complication at the bottom of the screen, for use by the companion editor screen
+    reader. -->
+    <string name="lower_complication_screen_reader_name">Lower complication.</string>
+
+    <!-- Name of the optional background complication which is drawn behind the watchface, for use
+    visually in the companion editor. [CHAR LIMIT=20] -->
+    <string name="background_complication_screen_name">Background</string>
+
+    <!-- Name of the optional background complication which is drawn behind the watchface, for use
+    by the companion editor screen reader. -->
+    <string name="background_complication_screen_reader_name">Background complication.</string>
+
+    <!-- Name of a style that controls the appearance of a digital clock [CHAR LIMIT=20] -->
+    <string name="digital_clock_style_name">Clock style</string>
+
+    <!-- Sub title for the menu option to select a widget that lets us configure the style of a
+    digital watch [CHAR LIMIT=20] -->
+    <string name="digital_clock_style_description">Clock style setting</string>
+
+    <!-- Menu option for a 12 hour digital clock display. [CHAR LIMIT=20] -->
+    <string name="digital_clock_style_12">12</string>
+
+    <!-- Menu option for a 12 hour digital clock display. [CHAR LIMIT=20] -->
+    <string name="digital_clock_style_12_screen_reader">12 hour clock</string>
+
+    <!-- Menu option for a 24 hour digital clock display used by screen reader. -->
+    <string name="digital_clock_style_24">24</string>
+
+    <!-- Menu option for a 24 hour digital clock display used by screen reader.-->
+    <string name="digital_clock_style_24_screen_reader">24 hour clock</string>
+
+    <!-- Menu option for selecting a digital clock [CHAR LIMIT=20] -->
+    <string name="style_digital_watch">Digital</string>
+
+    <!-- Menu option for selecting a digital clock from a screen reader. -->
+    <string name="style_digital_watch_screen_reader">Digital watch style</string>
+
+    <!-- Menu option for selecting an analog clock [CHAR LIMIT=20] -->
+    <string name="style_analog_watch">Analog</string>
+
+    <!-- Menu option for selecting an analog clock  from a screen reader.-->
+    <string name="style_analog_watch_screen_reader">Analog watch style </string>
+
+    <!-- Title for the menu option to select an analog or digital clock [CHAR LIMIT=20] -->
+    <string name="clock_type">Clock type</string>
+
+    <!-- Sub title for the menu option to select an analog or digital clock [CHAR LIMIT=20] -->
+    <string name="clock_type_description">Select analog or digital</string>
+
+    <!-- A menu option to select a widget that lets us configure the Complications (a watch
+    making term) for the digital watch [CHAR LIMIT=20] -->
+    <string name="digital_complications_setting">Complication</string>
+
+    <!-- Sub title for the menu option to select a widget that lets us configure the Complications
+    (a watch making term) for the digital watch [CHAR LIMIT=20] -->
+    <string name="digital_complications_setting_description">On or off</string>
+
+    <!-- List entry, enabling the complication for the digital watch. [CHAR LIMIT=20] -->
+    <string name="digital_complication_on_screen_name">On</string>
+
+    <!-- List entry, disabling the complication for the digital watch. [CHAR LIMIT=20] -->
+    <string name="digital_complication_off_screen_name">Off</string>
+
+    <!-- List entry setting the number of complications enabled for the analog watch.
+    [CHAR LIMIT=20] -->
+    <string name="analog_complication_one_screen_name">One</string>
+
+    <!-- List entry setting the number of complications enabled for the analog watch.
+    [CHAR LIMIT=20] -->
+    <string name="analog_complication_two_screen_name">Two</string>
+
+    <!-- List entry setting the number of complications enabled for the analog watch.
+    [CHAR LIMIT=20] -->
+    <string name="analog_complication_three_screen_name">Three</string>
+
+    <!-- Name of a complication at the top of the screen, for use visually in the companion editor.
+    [CHAR LIMIT=20] -->
+    <string name="hierarchical_complication1_screen_name">Top</string>
+
+    <!-- Name of a complication at the top of the screen, for use by the companion editor screen
+    reader. -->
+    <string name="hierarchical_complication1_screen_reader_name">Top complication</string>
+
+    <!-- Name of a complication at the middle of the screen, for use visually in the companion editor.
+    [CHAR LIMIT=20] -->
+    <string name="hierarchical_complication2_screen_name">Middle</string>
+
+    <!-- Name of a complication at the middle of the screen, for use by the companion editor screen
+    reader. -->
+    <string name="hierarchical_complication2_screen_reader_name">Middle complication</string>
+
+    <!-- Name of a complication at the bottom of the screen, for use visually in the companion editor.
+    [CHAR LIMIT=20] -->
+    <string name="hierarchical_complication3_screen_name">Bottom</string>
+
+    <!-- Name of a complication at the bottom of the screen, for use by the companion editor screen
+    reader. -->
+    <string name="hierarchical_complication3_screen_reader_name">Bottom complication</string>
 </resources>
\ No newline at end of file
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 84ead0a..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,
@@ -1419,7 +1429,10 @@
                 options.map {
                     (it as ComplicationSlotsOption).watchFaceEditorData?.toWireFormat() ?: Bundle()
                 },
-                options.map { (it as ComplicationSlotsOption).screenReaderName }
+                options.map {
+                    it as ComplicationSlotsOption
+                    it.screenReaderName ?: it.displayName
+                }
             )
 
         internal companion object {
@@ -1949,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),
@@ -2152,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,
@@ -2216,7 +2229,10 @@
                 affectedWatchFaceLayers.map { it.ordinal },
                 watchFaceEditorData?.toWireFormat(),
                 options.map { (it as ListOption).watchFaceEditorData?.toWireFormat() ?: Bundle() },
-                options.map { (it as ListOption).screenReaderName }
+                options.map {
+                    it as ListOption
+                    it.screenReaderName ?: it.displayName
+                }
             )
 
         internal companion object {
@@ -2733,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 135c472..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() {
@@ -424,6 +350,7 @@
         bitmap.assertAgainstGolden(screenshotRule, "active_screenshot")
     }
 
+    @FlakyTest(bugId = 264868778)
     @Test
     public fun testAmbientScreenshot() {
         handler.post(this::initCanvasWatchFace)
@@ -929,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/CancellableUniqueTask.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/CancellableUniqueTask.kt
index ea71d9b..76fdc41 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/CancellableUniqueTask.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/CancellableUniqueTask.kt
@@ -17,6 +17,7 @@
 package androidx.wear.watchface
 
 import android.os.Handler
+import java.time.Duration
 
 /**
  * Task posting helper which allows only one pending task at a time.
@@ -31,17 +32,13 @@
 
     fun isPending() = (pendingTask != null)
 
-    fun postUnique(task: () -> Unit) {
-        postDelayedUnique(0, task)
-    }
-
-    fun postDelayedUnique(delayMillis: Long, task: () -> Unit) {
+    fun postDelayedUnique(delay: Duration, task: () -> Unit) {
         cancel()
         val runnable = Runnable {
             task()
             pendingTask = null
         }
-        handler.postDelayed(runnable, delayMillis)
+        handler.postDelayed(runnable, delay.toMillis())
         pendingTask = runnable
     }
 }
\ No newline at end of file
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 b422068..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
@@ -33,7 +33,6 @@
 import androidx.wear.watchface.complications.data.ComplicationData
 import androidx.wear.watchface.complications.data.ComplicationExperimental
 import androidx.wear.watchface.complications.data.ComplicationType
-import androidx.wear.watchface.complications.data.NoDataComplicationData
 import androidx.wear.watchface.utility.TraceEvent
 import androidx.wear.watchface.control.data.IdTypeAndDefaultProviderPolicyWireFormat
 import androidx.wear.watchface.data.ComplicationStateWireFormat
@@ -200,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) {
@@ -355,13 +352,6 @@
         complication.setComplicationData(data, false, instant)
     }
 
-    @UiThread
-    internal fun clearComplicationData() {
-        for ((_, complication) in complicationSlots) {
-            complication.setComplicationData(NoDataComplicationData(), false, Instant.EPOCH)
-        }
-    }
-
     /**
      * For each slot, if the ComplicationData is timeline complication data then the correct
      * override is selected for [instant].
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 40c59a8..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
@@ -60,6 +61,7 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.launch
 import java.security.InvalidParameterException
+import java.time.Duration
 import java.time.Instant
 import java.time.ZoneId
 import java.time.ZonedDateTime
@@ -206,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)
             }
         }
     }
@@ -610,9 +612,6 @@
     internal var lastDrawTimeMillis: Long = 0
     internal var nextDrawTimeMillis: Long = 0
 
-    private val pendingUpdateTime: CancellableUniqueTask =
-        CancellableUniqueTask(watchFaceHostApi.getUiThreadHandler())
-
     internal val componentName =
         ComponentName(
             watchFaceHostApi.getContext().packageName,
@@ -729,7 +728,10 @@
         }
 
         if (!watchState.isHeadless) {
-            WatchFace.registerEditorDelegate(componentName, WFEditorDelegate())
+            WatchFace.registerEditorDelegate(
+                componentName,
+                WFEditorDelegate(headlessWatchFaceImpl = null)
+            )
             registerReceivers()
         }
 
@@ -780,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
 
@@ -851,15 +855,16 @@
             complicationSlotsManager.configExtrasChangeCallback = callback
         }
 
+        @SuppressLint("NewApi") // release
         override fun onDestroy(): Unit = TraceEvent("WFEditorDelegate.onDestroy").use {
             if (watchState.isHeadless) {
+                headlessWatchFaceImpl!!.release()
                 this@WatchFaceImpl.onDestroy()
             }
         }
     }
 
     internal fun onDestroy() {
-        pendingUpdateTime.cancel()
         renderer.onDestroyInternal()
         if (!watchState.isHeadless) {
             WatchFace.unregisterEditorDelegate(componentName)
@@ -898,9 +903,7 @@
         }
 
         if (renderer.shouldAnimate()) {
-            pendingUpdateTime.postUnique {
-                watchFaceHostApi.invalidate()
-            }
+            watchFaceHostApi.postInvalidate()
         }
     }
 
@@ -946,18 +949,19 @@
 
         if (renderer.shouldAnimate()) {
             val currentTimeMillis = systemTimeProvider.getSystemTimeMillis()
-            var delay = computeDelayTillNextFrame(startTimeMillis, currentTimeMillis, Instant.now())
-            nextDrawTimeMillis = currentTimeMillis + delay
+            var delayMillis =
+                computeDelayTillNextFrame(startTimeMillis, currentTimeMillis, Instant.now())
+            nextDrawTimeMillis = currentTimeMillis + delayMillis
 
             // We want to post our delayed task to post the choreographer frame a bit earlier than
             // the deadline because if we post it too close to the deadline we'll miss it. If we're
             // close to the deadline we post the choreographer frame immediately.
-            delay -= POST_CHOREOGRAPHER_FRAME_MILLIS_BEFORE_DEADLINE
+            delayMillis -= POST_CHOREOGRAPHER_FRAME_MILLIS_BEFORE_DEADLINE
 
-            if (delay <= 0) {
+            if (delayMillis <= 0) {
                 watchFaceHostApi.invalidate()
             } else {
-                pendingUpdateTime.postDelayedUnique(delay) { watchFaceHostApi.invalidate() }
+                watchFaceHostApi.postInvalidate(Duration.ofMillis(delayMillis))
             }
         }
     }
@@ -1190,7 +1194,6 @@
         writer.println("lastDrawTimeMillis=$lastDrawTimeMillis")
         writer.println("nextDrawTimeMillis=$nextDrawTimeMillis")
         writer.println("muteMode=$muteMode")
-        writer.println("pendingUpdateTime=${pendingUpdateTime.isPending()}")
         writer.println("lastTappedComplicationId=$lastTappedComplicationId")
         writer.println(
             "currentUserStyleRepository.userStyle=${currentUserStyleRepository.userStyle.value}"
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 30b5a82e..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
@@ -25,6 +25,7 @@
 import androidx.annotation.UiThread
 import androidx.wear.watchface.complications.SystemDataSources.DataSourceId
 import androidx.wear.watchface.style.data.UserStyleWireFormat
+import java.time.Duration
 import kotlinx.coroutines.CoroutineScope
 
 /**
@@ -36,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
 
@@ -124,6 +136,8 @@
     @UiThread
     public fun invalidate()
 
+    public fun postInvalidate(delay: Duration = Duration.ZERO)
+
     /** Intent to launch the complication permission denied activity. */
     public fun getComplicationDeniedIntent(): Intent?
 
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 3a46a56..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
@@ -95,6 +95,7 @@
 import java.io.ObjectInputStream
 import java.io.ObjectOutputStream
 import java.io.PrintWriter
+import java.time.Duration
 import java.time.Instant
 import java.time.ZoneId
 import kotlinx.coroutines.CancellationException
@@ -111,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
@@ -363,6 +361,7 @@
             UI,
             CURRENT
         }
+
         /**
          * Waits for deferredValue using runBlocking, then executes the task on the thread
          * specified by executionThread param.
@@ -388,6 +387,7 @@
                                     task(deferredValue)
                                 }
                             }
+
                             ExecutionThread.CURRENT -> {
                                 task(deferredValue)
                             }
@@ -713,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 {
@@ -727,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"
@@ -1199,6 +1206,9 @@
          */
         private var deferredSurfaceHolder = CompletableDeferred<SurfaceHolder>()
 
+        private val pendingUpdateTime: CancellableUniqueTask =
+            CancellableUniqueTask(getUiThreadHandler())
+
         internal val mutableWatchState = getMutableWatchState().apply {
             isVisible.value = this@EngineWrapper.isVisible || forceIsVisibleForTesting()
             // Watch faces with the old [onSetBinder] init flow don't know whether the system
@@ -1225,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]
@@ -1353,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.
@@ -1584,7 +1595,14 @@
         internal suspend fun updateInstance(newInstanceId: String) {
             val watchFaceImpl = deferredWatchFaceImpl.await()
             // If the favorite ID has changed then the complications are probably invalid.
-            watchFaceImpl.complicationSlotsManager.clearComplicationData()
+            setComplicationDataList(
+                watchFaceImpl.complicationSlotsManager.complicationSlots.map {
+                    IdAndComplicationDataWireFormat(
+                        it.key,
+                        NoDataComplicationData().asWireComplicationData()
+                    )
+                }
+            )
 
             // However we may have valid complications cached.
             readComplicationDataCache(_context, newInstanceId)?.let {
@@ -1685,6 +1703,7 @@
         @UiThread
         override fun onDestroy(): Unit = TraceEvent("EngineWrapper.onDestroy").use {
             super.onDestroy()
+            pendingUpdateTime.cancel()
             if (!mutableWatchState.isHeadless) {
                 mainThreadPriorityDelegate.setNormalPriority()
             }
@@ -1755,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
             }
@@ -1764,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()
@@ -1798,6 +1823,7 @@
                             )
                         )
                     }
+
                 Constants.COMMAND_TOUCH ->
                     uiThreadCoroutineScope.runBlockingWithTracing("onCommand COMMAND_TOUCH") {
                         val watchFaceImpl = deferredWatchFaceImpl.await()
@@ -1812,6 +1838,7 @@
                             )
                         )
                     }
+
                 Constants.COMMAND_TOUCH_CANCEL ->
                     uiThreadCoroutineScope.runBlockingWithTracing(
                         "onCommand COMMAND_TOUCH_CANCEL"
@@ -1828,6 +1855,7 @@
                             )
                         )
                     }
+
                 else -> {
                 }
             }
@@ -2062,6 +2090,7 @@
                     TraceEvent("WatchFaceService.createComplicationsManager").use {
                         createComplicationSlotsManager(currentUserStyleRepository)
                     }
+                complicationSlotsManager.watchFaceHostApi = this@EngineWrapper
                 complicationSlotsManager.watchState = watchState
                 complicationSlotsManager.listenForStyleChanges(uiThreadCoroutineScope)
                 listenForComplicationChanges(complicationSlotsManager)
@@ -2175,7 +2204,7 @@
                 deferredWatchFaceImpl,
                 uiThreadCoroutineScope,
                 _context.contentResolver,
-                !isPreAndroidR()
+                wearSdkVersion >= Build.VERSION_CODES.R
             )
 
             // There's no point creating BroadcastsReceiver or listening for Accessibility state
@@ -2286,7 +2315,7 @@
             // to render soon anyway.
             var initFinished = false
             complicationSlotsManager.init(
-                this, renderer,
+                renderer,
                 object : ComplicationSlot.InvalidateListener {
                     @SuppressWarnings("SyntheticAccessor")
                     override fun onInvalidate() {
@@ -2311,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
@@ -2368,6 +2397,10 @@
             }
         }
 
+        override fun postInvalidate(delay: Duration) {
+            pendingUpdateTime.postDelayedUnique(delay) { invalidate() }
+        }
+
         override fun getComplicationDeniedIntent() =
             getWatchFaceImplOrNull()?.complicationDeniedDialogIntent
 
@@ -2618,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)
@@ -2681,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")
             }
 
@@ -2693,8 +2731,7 @@
                 )
                 if (wslFlow.iWatchFaceService.asBinder().isBinderAlive) {
                     writer.println(
-                        "iWatchFaceService.apiVersion=" +
-                            "${wslFlow.iWatchFaceService.apiVersion}"
+                        "iWatchFaceService.apiVersion=${wslFlow.iWatchFaceService.apiVersion}"
                     )
                 }
             }
@@ -2712,13 +2749,12 @@
             writer.println("frameCallbackPending=$frameCallbackPending")
             writer.println("destroyed=$destroyed")
             writer.println("surfaceDestroyed=$surfaceDestroyed")
-            writer.println(
-                "lastComplications=" + complicationsFlow.value.joinToString()
-            )
+            writer.println("lastComplications=${complicationsFlow.value.joinToString()}")
+            writer.println("pendingUpdateTime=${pendingUpdateTime.isPending()}")
 
             synchronized(lock) {
                 forEachListener("dump") {
-                    writer.println("listener = " + it.asBinder())
+                    writer.println("listener = ${it.asBinder()}")
                 }
             }
 
@@ -2736,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 bfb315b..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
             )
         )
@@ -5915,9 +6084,72 @@
         InteractiveInstanceManager.releaseInstance(NEW_ID)
     }
 
+    @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
+    public fun updateComplications_after_updateInstance() {
+        val complicationList = listOf(
+            IdAndComplicationDataWireFormat(
+                LEFT_COMPLICATION_ID,
+                WireComplicationData.Builder(WireComplicationData.TYPE_LONG_TEXT)
+                    .setLongText(WireComplicationText.plainText("TYPE_LONG_TEXT")).build()
+            ),
+            IdAndComplicationDataWireFormat(
+                RIGHT_COMPLICATION_ID,
+                WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                    .setShortText(WireComplicationText.plainText("TYPE_SHORT_TEXT")).build()
+            )
+        )
+
+        initWallpaperInteractiveWatchFaceInstance(
+            WatchFaceType.ANALOG,
+            listOf(leftComplication, rightComplication),
+            UserStyleSchema(emptyList()),
+            WallpaperInteractiveWatchFaceInstanceParams(
+                INTERACTIVE_INSTANCE_ID,
+                DeviceConfig(
+                    false,
+                    false,
+                    0,
+                    0
+                ),
+                WatchUiState(false, 0),
+                UserStyle(emptyMap()).toWireFormat(),
+                null,
+                null,
+                null
+            )
+        )
+
+        interactiveWatchFaceInstance.updateComplicationData(complicationList)
+
+        val NEW_ID = SYSTEM_SUPPORTS_CONSISTENT_IDS_PREFIX + "New"
+        runBlocking {
+            interactiveWatchFaceInstance.updateWatchfaceInstance(
+                NEW_ID,
+                UserStyleWireFormat(emptyMap())
+            )
+        }
+
+        assertThat(leftComplication.complicationData.value).isInstanceOf(
+            NoDataComplicationData::class.java
+        )
+        assertThat(rightComplication.complicationData.value).isInstanceOf(
+            NoDataComplicationData::class.java
+        )
+
+        interactiveWatchFaceInstance.updateComplicationData(complicationList)
+
+        assertThat(leftComplication.complicationData.value).isInstanceOf(
+            LongTextComplicationData::class.java
+        )
+        assertThat(rightComplication.complicationData.value).isInstanceOf(
+            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
@@ -5942,7 +6174,8 @@
                         canvas: Canvas,
                         bounds: Rect,
                         zonedDateTime: ZonedDateTime
-                    ) { }
+                    ) {
+                    }
 
                     override fun renderHighlightLayer(
                         canvas: Canvas,
@@ -5957,7 +6190,6 @@
             null,
             handler,
             null,
-            false,
             null,
             choreographer,
             forceIsVisible = true
@@ -5977,6 +6209,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -6000,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()
@@ -6041,6 +6274,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun onPreviewImageUpdateRequested() {
         @Suppress("DEPRECATION")
         lateinit var renderer: Renderer.CanvasRenderer
@@ -6060,7 +6294,8 @@
                         canvas: Canvas,
                         bounds: Rect,
                         zonedDateTime: ZonedDateTime
-                    ) { }
+                    ) {
+                    }
 
                     override fun renderHighlightLayer(
                         canvas: Canvas,
@@ -6075,7 +6310,6 @@
             null,
             handler,
             null,
-            false,
             null,
             choreographer,
             forceIsVisible = true
@@ -6095,6 +6329,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -6118,7 +6353,7 @@
         val listener = object : IWatchfaceListener.Stub() {
             override fun getApiVersion() = 1
 
-            override fun onWatchfaceReady() { }
+            override fun onWatchfaceReady() {}
 
             override fun onWatchfaceColorsChanged(watchFaceColors: WatchFaceColorsWireFormat?) {}
 
@@ -6160,6 +6395,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     public fun onPreviewImageUpdateRequested_earlyCall() {
         @Suppress("DEPRECATION")
         lateinit var renderer: Renderer.CanvasRenderer
@@ -6199,7 +6435,6 @@
             null,
             handler,
             null,
-            false,
             null,
             choreographer,
             forceIsVisible = true
@@ -6219,6 +6454,7 @@
                         WatchUiState(false, 0),
                         UserStyle(emptyMap()).toWireFormat(),
                         emptyList(),
+                        null,
                         null
                     ),
                     object : IPendingInteractiveWatchFace.Stub() {
@@ -6265,6 +6501,7 @@
     }
 
     @Test
+    @Config(sdk = [Build.VERSION_CODES.R])
     @RequiresApi(Build.VERSION_CODES.O_MR1)
     public fun sendPreviewImageNeedsUpdateRequest_headlessInstance() {
         @Suppress("DEPRECATION")
@@ -6284,11 +6521,13 @@
                     init {
                         sendPreviewImageNeedsUpdateRequest()
                     }
+
                     override fun render(
                         canvas: Canvas,
                         bounds: Rect,
                         zonedDateTime: ZonedDateTime
-                    ) { }
+                    ) {
+                    }
 
                     override fun renderHighlightLayer(
                         canvas: Canvas,
@@ -6303,7 +6542,6 @@
             null,
             handler,
             null,
-            false,
             null,
             choreographer,
             forceIsVisible = true
@@ -6324,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,
@@ -6348,6 +6587,7 @@
                 WatchUiState(false, 0),
                 UserStyle(emptyMap()).toWireFormat(),
                 null,
+                null,
                 null
             )
         )
@@ -6361,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
@@ -6384,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
@@ -6406,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/window/window/api/current.txt b/window/window/api/current.txt
index f0f8603..4c43822 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -50,6 +50,19 @@
     property public final boolean isEmpty;
   }
 
+  public final class EmbeddingAspectRatio {
+    method public static androidx.window.embedding.EmbeddingAspectRatio alwaysAllow();
+    method public static androidx.window.embedding.EmbeddingAspectRatio alwaysDisallow();
+    method public static androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
+    field public static final androidx.window.embedding.EmbeddingAspectRatio.Companion Companion;
+  }
+
+  public static final class EmbeddingAspectRatio.Companion {
+    method public androidx.window.embedding.EmbeddingAspectRatio alwaysAllow();
+    method public androidx.window.embedding.EmbeddingAspectRatio alwaysDisallow();
+    method public androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
+  }
+
   public abstract class EmbeddingRule {
   }
 
@@ -121,6 +134,8 @@
     method public androidx.window.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int finishPrimaryWithSecondary);
     method public androidx.window.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int finishSecondaryWithPrimary);
     method public androidx.window.embedding.SplitPairRule.Builder setLayoutDirection(int layoutDirection);
+    method public androidx.window.embedding.SplitPairRule.Builder setMaxAspectRatioInLandscape(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
+    method public androidx.window.embedding.SplitPairRule.Builder setMaxAspectRatioInPortrait(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
     method public androidx.window.embedding.SplitPairRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
     method public androidx.window.embedding.SplitPairRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
     method public androidx.window.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
@@ -142,6 +157,8 @@
     method public androidx.window.embedding.SplitPlaceholderRule build();
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int finishPrimaryWithPlaceholder);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int layoutDirection);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setMaxAspectRatioInLandscape(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setMaxAspectRatioInPortrait(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
@@ -150,18 +167,25 @@
 
   public class SplitRule extends androidx.window.embedding.EmbeddingRule {
     method public final int getLayoutDirection();
+    method public final androidx.window.embedding.EmbeddingAspectRatio getMaxAspectRatioInLandscape();
+    method public final androidx.window.embedding.EmbeddingAspectRatio getMaxAspectRatioInPortrait();
     method public final int getMinSmallestWidthDp();
     method public final int getMinWidthDp();
     method public final float getSplitRatio();
     property public final int layoutDirection;
+    property public final androidx.window.embedding.EmbeddingAspectRatio maxAspectRatioInLandscape;
+    property public final androidx.window.embedding.EmbeddingAspectRatio maxAspectRatioInPortrait;
     property public final int minSmallestWidthDp;
     property public final int minWidthDp;
     property public final float splitRatio;
     field public static final androidx.window.embedding.SplitRule.Companion Companion;
-    field public static final int DEFAULT_SPLIT_MIN_DIMENSION_DP = 600; // 0x258
     field public static final int FINISH_ADJACENT = 2; // 0x2
     field public static final int FINISH_ALWAYS = 1; // 0x1
     field public static final int FINISH_NEVER = 0; // 0x0
+    field public static final androidx.window.embedding.EmbeddingAspectRatio SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT;
+    field public static final androidx.window.embedding.EmbeddingAspectRatio SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT;
+    field public static final int SPLIT_MIN_DIMENSION_ALWAYS_ALLOW = 0; // 0x0
+    field public static final int SPLIT_MIN_DIMENSION_DP_DEFAULT = 600; // 0x258
   }
 
   public static final class SplitRule.Companion {
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index f4e24a6..da9c818 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -57,6 +57,19 @@
     property public final boolean isEmpty;
   }
 
+  public final class EmbeddingAspectRatio {
+    method public static androidx.window.embedding.EmbeddingAspectRatio alwaysAllow();
+    method public static androidx.window.embedding.EmbeddingAspectRatio alwaysDisallow();
+    method public static androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
+    field public static final androidx.window.embedding.EmbeddingAspectRatio.Companion Companion;
+  }
+
+  public static final class EmbeddingAspectRatio.Companion {
+    method public androidx.window.embedding.EmbeddingAspectRatio alwaysAllow();
+    method public androidx.window.embedding.EmbeddingAspectRatio alwaysDisallow();
+    method public androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
+  }
+
   public abstract class EmbeddingRule {
   }
 
@@ -128,6 +141,8 @@
     method public androidx.window.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int finishPrimaryWithSecondary);
     method public androidx.window.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int finishSecondaryWithPrimary);
     method public androidx.window.embedding.SplitPairRule.Builder setLayoutDirection(int layoutDirection);
+    method public androidx.window.embedding.SplitPairRule.Builder setMaxAspectRatioInLandscape(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
+    method public androidx.window.embedding.SplitPairRule.Builder setMaxAspectRatioInPortrait(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
     method public androidx.window.embedding.SplitPairRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
     method public androidx.window.embedding.SplitPairRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
     method public androidx.window.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
@@ -149,6 +164,8 @@
     method public androidx.window.embedding.SplitPlaceholderRule build();
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int finishPrimaryWithPlaceholder);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int layoutDirection);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setMaxAspectRatioInLandscape(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setMaxAspectRatioInPortrait(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
@@ -157,18 +174,25 @@
 
   public class SplitRule extends androidx.window.embedding.EmbeddingRule {
     method public final int getLayoutDirection();
+    method public final androidx.window.embedding.EmbeddingAspectRatio getMaxAspectRatioInLandscape();
+    method public final androidx.window.embedding.EmbeddingAspectRatio getMaxAspectRatioInPortrait();
     method public final int getMinSmallestWidthDp();
     method public final int getMinWidthDp();
     method public final float getSplitRatio();
     property public final int layoutDirection;
+    property public final androidx.window.embedding.EmbeddingAspectRatio maxAspectRatioInLandscape;
+    property public final androidx.window.embedding.EmbeddingAspectRatio maxAspectRatioInPortrait;
     property public final int minSmallestWidthDp;
     property public final int minWidthDp;
     property public final float splitRatio;
     field public static final androidx.window.embedding.SplitRule.Companion Companion;
-    field public static final int DEFAULT_SPLIT_MIN_DIMENSION_DP = 600; // 0x258
     field public static final int FINISH_ADJACENT = 2; // 0x2
     field public static final int FINISH_ALWAYS = 1; // 0x1
     field public static final int FINISH_NEVER = 0; // 0x0
+    field public static final androidx.window.embedding.EmbeddingAspectRatio SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT;
+    field public static final androidx.window.embedding.EmbeddingAspectRatio SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT;
+    field public static final int SPLIT_MIN_DIMENSION_ALWAYS_ALLOW = 0; // 0x0
+    field public static final int SPLIT_MIN_DIMENSION_DP_DEFAULT = 600; // 0x258
   }
 
   public static final class SplitRule.Companion {
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index f0f8603..4c43822 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -50,6 +50,19 @@
     property public final boolean isEmpty;
   }
 
+  public final class EmbeddingAspectRatio {
+    method public static androidx.window.embedding.EmbeddingAspectRatio alwaysAllow();
+    method public static androidx.window.embedding.EmbeddingAspectRatio alwaysDisallow();
+    method public static androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
+    field public static final androidx.window.embedding.EmbeddingAspectRatio.Companion Companion;
+  }
+
+  public static final class EmbeddingAspectRatio.Companion {
+    method public androidx.window.embedding.EmbeddingAspectRatio alwaysAllow();
+    method public androidx.window.embedding.EmbeddingAspectRatio alwaysDisallow();
+    method public androidx.window.embedding.EmbeddingAspectRatio ratio(@FloatRange(from=1.0, fromInclusive=false) float ratio);
+  }
+
   public abstract class EmbeddingRule {
   }
 
@@ -121,6 +134,8 @@
     method public androidx.window.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int finishPrimaryWithSecondary);
     method public androidx.window.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int finishSecondaryWithPrimary);
     method public androidx.window.embedding.SplitPairRule.Builder setLayoutDirection(int layoutDirection);
+    method public androidx.window.embedding.SplitPairRule.Builder setMaxAspectRatioInLandscape(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
+    method public androidx.window.embedding.SplitPairRule.Builder setMaxAspectRatioInPortrait(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
     method public androidx.window.embedding.SplitPairRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
     method public androidx.window.embedding.SplitPairRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
     method public androidx.window.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
@@ -142,6 +157,8 @@
     method public androidx.window.embedding.SplitPlaceholderRule build();
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int finishPrimaryWithPlaceholder);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int layoutDirection);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setMaxAspectRatioInLandscape(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setMaxAspectRatioInPortrait(androidx.window.embedding.EmbeddingAspectRatio aspectRatio);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setMinSmallestWidthDp(@IntRange(from=0L) int minSmallestWidthDp);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setMinWidthDp(@IntRange(from=0L) int minWidthDp);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
@@ -150,18 +167,25 @@
 
   public class SplitRule extends androidx.window.embedding.EmbeddingRule {
     method public final int getLayoutDirection();
+    method public final androidx.window.embedding.EmbeddingAspectRatio getMaxAspectRatioInLandscape();
+    method public final androidx.window.embedding.EmbeddingAspectRatio getMaxAspectRatioInPortrait();
     method public final int getMinSmallestWidthDp();
     method public final int getMinWidthDp();
     method public final float getSplitRatio();
     property public final int layoutDirection;
+    property public final androidx.window.embedding.EmbeddingAspectRatio maxAspectRatioInLandscape;
+    property public final androidx.window.embedding.EmbeddingAspectRatio maxAspectRatioInPortrait;
     property public final int minSmallestWidthDp;
     property public final int minWidthDp;
     property public final float splitRatio;
     field public static final androidx.window.embedding.SplitRule.Companion Companion;
-    field public static final int DEFAULT_SPLIT_MIN_DIMENSION_DP = 600; // 0x258
     field public static final int FINISH_ADJACENT = 2; // 0x2
     field public static final int FINISH_ALWAYS = 1; // 0x1
     field public static final int FINISH_NEVER = 0; // 0x0
+    field public static final androidx.window.embedding.EmbeddingAspectRatio SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT;
+    field public static final androidx.window.embedding.EmbeddingAspectRatio SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT;
+    field public static final int SPLIT_MIN_DIMENSION_ALWAYS_ALLOW = 0; // 0x0
+    field public static final int SPLIT_MIN_DIMENSION_DP_DEFAULT = 600; // 0x258
   }
 
   public static final class SplitRule.Companion {
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt
index cb44e6b..8203078 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt
@@ -22,10 +22,16 @@
 import android.graphics.Rect
 import android.util.LayoutDirection
 import androidx.test.core.app.ApplicationProvider
-import androidx.window.embedding.SplitRule.Companion.DEFAULT_SPLIT_MIN_DIMENSION_DP
+import androidx.window.embedding.EmbeddingAspectRatio.Companion.alwaysAllow
+import androidx.window.embedding.EmbeddingAspectRatio.Companion.alwaysDisallow
+import androidx.window.embedding.EmbeddingAspectRatio.Companion.ratio
+import androidx.window.embedding.SplitRule.Companion.SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
+import androidx.window.embedding.SplitRule.Companion.SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT
+import androidx.window.embedding.SplitRule.Companion.SPLIT_MIN_DIMENSION_DP_DEFAULT
 import androidx.window.embedding.SplitRule.Companion.FINISH_ADJACENT
 import androidx.window.embedding.SplitRule.Companion.FINISH_ALWAYS
 import androidx.window.embedding.SplitRule.Companion.FINISH_NEVER
+import androidx.window.embedding.SplitRule.Companion.SPLIT_MIN_DIMENSION_ALWAYS_ALLOW
 import androidx.window.test.R
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
@@ -52,6 +58,10 @@
             .parseRules(application, R.xml.test_split_config_default_split_pair_rule)
         assertEquals(1, rules.size)
         val rule: SplitPairRule = rules.first() as SplitPairRule
+        assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minWidthDp)
+        assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minSmallestWidthDp)
+        assertEquals(SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT, rule.maxAspectRatioInPortrait)
+        assertEquals(SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT, rule.maxAspectRatioInLandscape)
         assertEquals(FINISH_NEVER, rule.finishPrimaryWithSecondary)
         assertEquals(FINISH_ALWAYS, rule.finishSecondaryWithPrimary)
         assertEquals(false, rule.clearTop)
@@ -62,12 +72,37 @@
     }
 
     /**
+     * Verifies that params are set correctly when reading {@link SplitPairRule} from XML.
+     * @see R.xml.test_split_config_custom_split_pair_rule for customized value.
+     */
+    @Test
+    fun testCustom_SplitPairRule_Xml() {
+        val rules = RuleController
+            .parseRules(application, R.xml.test_split_config_custom_split_pair_rule)
+        assertEquals(1, rules.size)
+        val rule: SplitPairRule = rules.first() as SplitPairRule
+        assertEquals(123, rule.minWidthDp)
+        assertEquals(456, rule.minSmallestWidthDp)
+        assertEquals(1.23f, rule.maxAspectRatioInPortrait.value)
+        assertEquals(alwaysDisallow(), rule.maxAspectRatioInLandscape)
+        assertEquals(FINISH_ALWAYS, rule.finishPrimaryWithSecondary)
+        assertEquals(FINISH_NEVER, rule.finishSecondaryWithPrimary)
+        assertEquals(true, rule.clearTop)
+        assertEquals(0.1f, rule.splitRatio)
+        assertEquals(LayoutDirection.RTL, rule.layoutDirection)
+    }
+
+    /**
      * Verifies that default params are set correctly when creating {@link SplitPairRule} with a
      * builder.
      */
     @Test
     fun testDefaults_SplitPairRule_Builder() {
         val rule = SplitPairRule.Builder(HashSet()).build()
+        assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minWidthDp)
+        assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minSmallestWidthDp)
+        assertEquals(SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT, rule.maxAspectRatioInPortrait)
+        assertEquals(SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT, rule.maxAspectRatioInLandscape)
         assertEquals(FINISH_NEVER, rule.finishPrimaryWithSecondary)
         assertEquals(FINISH_ALWAYS, rule.finishSecondaryWithPrimary)
         assertEquals(false, rule.clearTop)
@@ -94,6 +129,8 @@
         val rule = SplitPairRule.Builder(filters)
             .setMinWidthDp(123)
             .setMinSmallestWidthDp(456)
+            .setMaxAspectRatioInPortrait(ratio(1.23f))
+            .setMaxAspectRatioInLandscape(ratio(4.56f))
             .setFinishPrimaryWithSecondary(FINISH_ADJACENT)
             .setFinishSecondaryWithPrimary(FINISH_ADJACENT)
             .setClearTop(true)
@@ -108,6 +145,8 @@
         assertEquals(filters, rule.filters)
         assertEquals(123, rule.minWidthDp)
         assertEquals(456, rule.minSmallestWidthDp)
+        assertEquals(1.23f, rule.maxAspectRatioInPortrait.value)
+        assertEquals(4.56f, rule.maxAspectRatioInLandscape.value)
     }
 
     /**
@@ -142,6 +181,126 @@
                 .setSplitRatio(1.1f)
                 .build()
         }
+        assertThrows(IllegalArgumentException::class.java) {
+            SplitPairRule.Builder(HashSet())
+                .setMaxAspectRatioInPortrait(ratio(-1f))
+                .build()
+        }
+        assertThrows(IllegalArgumentException::class.java) {
+            SplitPairRule.Builder(HashSet())
+                .setMaxAspectRatioInLandscape(ratio(-1f))
+                .build()
+        }
+    }
+
+    /**
+     * Verifies that the SplitPairRule verifies that the parent bounds satisfy
+     * maxAspectRatioInPortrait.
+     */
+    @Test
+    fun testSplitPairRule_maxAspectRatioInPortrait() {
+        // Always allow split
+        var rule = SplitPairRule.Builder(HashSet())
+            .setMinWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMinSmallestWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMaxAspectRatioInLandscape(alwaysAllow())
+            .setMaxAspectRatioInPortrait(alwaysAllow())
+            .build()
+        var width = 100
+        var height = 1000
+        var bounds = Rect(0, 0, width, height)
+        assertTrue(rule.checkParentBounds(density, bounds))
+
+        // Always disallow split in portrait
+        rule = SplitPairRule.Builder(HashSet())
+            .setMinWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMinSmallestWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMaxAspectRatioInLandscape(alwaysAllow())
+            .setMaxAspectRatioInPortrait(alwaysDisallow())
+            .build()
+        width = 100
+        height = 101
+        bounds = Rect(0, 0, width, height)
+        assertFalse(rule.checkParentBounds(density, bounds))
+        // Ignore if the bounds in landscape
+        bounds = Rect(0, 0, height, width)
+        assertTrue(rule.checkParentBounds(density, bounds))
+
+        // Compare the aspect ratio in portrait
+        rule = SplitPairRule.Builder(HashSet())
+            .setMinWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMinSmallestWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMaxAspectRatioInLandscape(alwaysAllow())
+            .setMaxAspectRatioInPortrait(ratio(1.1f))
+            .build()
+        // Equals to the max aspect ratio
+        width = 100
+        height = 110
+        bounds = Rect(0, 0, width, height)
+        assertTrue(rule.checkParentBounds(density, bounds))
+        // Greater than the max aspect ratio
+        width = 100
+        height = 111
+        bounds = Rect(0, 0, width, height)
+        assertFalse(rule.checkParentBounds(density, bounds))
+        // Ignore if the bounds in landscape
+        bounds = Rect(0, 0, height, width)
+        assertTrue(rule.checkParentBounds(density, bounds))
+    }
+
+    /**
+     * Verifies that the SplitPairRule verifies that the parent bounds satisfy
+     * maxAspectRatioInLandscape.
+     */
+    @Test
+    fun testSplitPairRule_maxAspectRatioInLandscape() {
+        // Always allow split
+        var rule = SplitPairRule.Builder(HashSet())
+            .setMinWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMinSmallestWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMaxAspectRatioInPortrait(alwaysAllow())
+            .setMaxAspectRatioInLandscape(alwaysAllow())
+            .build()
+        var width = 1000
+        var height = 100
+        var bounds = Rect(0, 0, width, height)
+        assertTrue(rule.checkParentBounds(density, bounds))
+
+        // Always disallow split in landscape
+        rule = SplitPairRule.Builder(HashSet())
+            .setMinWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMinSmallestWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMaxAspectRatioInPortrait(alwaysAllow())
+            .setMaxAspectRatioInLandscape(alwaysDisallow())
+            .build()
+        width = 101
+        height = 100
+        bounds = Rect(0, 0, width, height)
+        assertFalse(rule.checkParentBounds(density, bounds))
+        // Ignore if the bounds in portrait
+        bounds = Rect(0, 0, height, width)
+        assertTrue(rule.checkParentBounds(density, bounds))
+
+        // Compare the aspect ratio in landscape
+        rule = SplitPairRule.Builder(HashSet())
+            .setMinWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMinSmallestWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMaxAspectRatioInPortrait(alwaysAllow())
+            .setMaxAspectRatioInLandscape(ratio(1.1f))
+            .build()
+        // Equals to the max aspect ratio
+        width = 110
+        height = 100
+        bounds = Rect(0, 0, width, height)
+        assertTrue(rule.checkParentBounds(density, bounds))
+        // Greater than the max aspect ratio
+        width = 111
+        height = 100
+        bounds = Rect(0, 0, width, height)
+        assertFalse(rule.checkParentBounds(density, bounds))
+        // Ignore if the bounds in portrait
+        bounds = Rect(0, 0, height, width)
+        assertTrue(rule.checkParentBounds(density, bounds))
     }
 
     /**
@@ -154,6 +313,10 @@
             .parseRules(application, R.xml.test_split_config_default_split_placeholder_rule)
         assertEquals(1, rules.size)
         val rule: SplitPlaceholderRule = rules.first() as SplitPlaceholderRule
+        assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minWidthDp)
+        assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minSmallestWidthDp)
+        assertEquals(SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT, rule.maxAspectRatioInPortrait)
+        assertEquals(SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT, rule.maxAspectRatioInLandscape)
         assertEquals(FINISH_ALWAYS, rule.finishPrimaryWithPlaceholder)
         assertEquals(false, rule.isSticky)
         assertEquals(0.5f, rule.splitRatio)
@@ -163,19 +326,42 @@
     }
 
     /**
+     * Verifies that params are set correctly when reading {@link SplitPlaceholderRule} from XML.
+     * @see R.xml.test_split_config_custom_split_placeholder_rule for customized value.
+     */
+    @Test
+    fun testCustom_SplitPlaceholderRule_Xml() {
+        val rules = RuleController
+            .parseRules(application, R.xml.test_split_config_custom_split_placeholder_rule)
+        assertEquals(1, rules.size)
+        val rule: SplitPlaceholderRule = rules.first() as SplitPlaceholderRule
+        assertEquals(123, rule.minWidthDp)
+        assertEquals(456, rule.minSmallestWidthDp)
+        assertEquals(1.23f, rule.maxAspectRatioInPortrait.value)
+        assertEquals(alwaysDisallow(), rule.maxAspectRatioInLandscape)
+        assertEquals(FINISH_ADJACENT, rule.finishPrimaryWithPlaceholder)
+        assertEquals(true, rule.isSticky)
+        assertEquals(0.1f, rule.splitRatio)
+        assertEquals(LayoutDirection.RTL, rule.layoutDirection)
+    }
+
+    /**
      * Verifies that default params are set correctly when creating {@link SplitPlaceholderRule}
      * with a builder.
      */
     @Test
     fun testDefaults_SplitPlaceholderRule_Builder() {
-        val rule = SplitPlaceholderRule.Builder(HashSet(), Intent())
-            .setMinWidthDp(123)
-            .setMinSmallestWidthDp(456)
-            .build()
+        val rule = SplitPlaceholderRule.Builder(HashSet(), Intent()).build()
+        assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minWidthDp)
+        assertEquals(SPLIT_MIN_DIMENSION_DP_DEFAULT, rule.minSmallestWidthDp)
+        assertEquals(SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT, rule.maxAspectRatioInPortrait)
+        assertEquals(SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT, rule.maxAspectRatioInLandscape)
         assertEquals(FINISH_ALWAYS, rule.finishPrimaryWithPlaceholder)
         assertEquals(false, rule.isSticky)
         assertEquals(0.5f, rule.splitRatio)
         assertEquals(LayoutDirection.LOCALE, rule.layoutDirection)
+        assertTrue(rule.checkParentBounds(density, minValidWindowBounds()))
+        assertFalse(rule.checkParentBounds(density, almostValidWindowBounds()))
     }
 
     /**
@@ -195,6 +381,8 @@
         val rule = SplitPlaceholderRule.Builder(filters, intent)
             .setMinWidthDp(123)
             .setMinSmallestWidthDp(456)
+            .setMaxAspectRatioInPortrait(ratio(1.23f))
+            .setMaxAspectRatioInLandscape(ratio(4.56f))
             .setFinishPrimaryWithPlaceholder(FINISH_ADJACENT)
             .setSticky(true)
             .setSplitRatio(0.3f)
@@ -208,6 +396,8 @@
         assertEquals(intent, rule.placeholderIntent)
         assertEquals(123, rule.minWidthDp)
         assertEquals(456, rule.minSmallestWidthDp)
+        assertEquals(1.23f, rule.maxAspectRatioInPortrait.value)
+        assertEquals(4.56f, rule.maxAspectRatioInLandscape.value)
     }
 
     /**
@@ -249,6 +439,129 @@
                 .setSplitRatio(1.1f)
                 .build()
         }
+        assertThrows(IllegalArgumentException::class.java) {
+            SplitPairRule.Builder(HashSet())
+                .setMaxAspectRatioInPortrait(ratio(-1f))
+                .build()
+        }
+        assertThrows(IllegalArgumentException::class.java) {
+            SplitPairRule.Builder(HashSet())
+                .setMaxAspectRatioInLandscape(ratio(-1f))
+                .build()
+        }
+    }
+
+    /**
+     * Verifies that the SplitPlaceholderRule verifies that the parent bounds satisfy
+     * maxAspectRatioInPortrait.
+     */
+    @Test
+    fun testSplitPlaceholderRule_maxAspectRatioInPortrait() {
+        // Always allow split
+        var rule = SplitPlaceholderRule.Builder(HashSet(), Intent())
+            .setMinWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMinSmallestWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMaxAspectRatioInLandscape(alwaysAllow())
+            .setMaxAspectRatioInPortrait(alwaysAllow())
+            .build()
+        var width = 100
+        var height = 1000
+        var bounds = Rect(0, 0, width, height)
+        assertTrue(rule.checkParentBounds(density, bounds))
+
+        // Always disallow split in portrait
+        rule = SplitPlaceholderRule.Builder(HashSet(), Intent())
+            .setMinWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMinSmallestWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMaxAspectRatioInLandscape(alwaysAllow())
+            .setMaxAspectRatioInPortrait(alwaysDisallow())
+            .build()
+        width = 100
+        height = 101
+        bounds = Rect(0, 0, width, height)
+        assertFalse(rule.checkParentBounds(density, bounds))
+        // Ignore if the bounds in landscape
+        bounds = Rect(0, 0, height, width)
+        assertTrue(rule.checkParentBounds(density, bounds))
+
+        // Compare the aspect ratio in portrait
+        rule = SplitPlaceholderRule.Builder(HashSet(), Intent())
+            .setMinWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMinSmallestWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMaxAspectRatioInLandscape(alwaysAllow())
+            .setMaxAspectRatioInPortrait(ratio(1.1f))
+            .build()
+        // Equals to the max aspect ratio
+        width = 100
+        height = 110
+        bounds = Rect(0, 0, width, height)
+        assertTrue(rule.checkParentBounds(density, bounds))
+        // Greater than the max aspect ratio
+        width = 100
+        height = 111
+        bounds = Rect(0, 0, width, height)
+        assertFalse(rule.checkParentBounds(density, bounds))
+        // Ignore if the bounds in landscape
+        bounds = Rect(0, 0, height, width)
+        assertTrue(rule.checkParentBounds(density, bounds))
+    }
+
+    /**
+     * Verifies that the SplitPlaceholderRule verifies that the parent bounds satisfy
+     * maxAspectRatioInLandscape.
+     */
+    @Test
+    fun testSplitPlaceholderRule_maxAspectRatioInLandscape() {
+        // Always allow split
+        var rule = SplitPlaceholderRule.Builder(HashSet(), Intent())
+            .setMinWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMinSmallestWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMaxAspectRatioInPortrait(alwaysAllow())
+            .setMaxAspectRatioInLandscape(alwaysAllow())
+            .build()
+        var width = 1000
+        var height = 100
+        var bounds = Rect(0, 0, width, height)
+        assertTrue(rule.checkParentBounds(density, bounds))
+        // Ignore if the bounds in portrait
+        bounds = Rect(0, 0, height, width)
+        assertTrue(rule.checkParentBounds(density, bounds))
+
+        // Always disallow split in landscape
+        rule = SplitPlaceholderRule.Builder(HashSet(), Intent())
+            .setMinWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMinSmallestWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMaxAspectRatioInPortrait(alwaysAllow())
+            .setMaxAspectRatioInLandscape(alwaysDisallow())
+            .build()
+        width = 101
+        height = 100
+        bounds = Rect(0, 0, width, height)
+        assertFalse(rule.checkParentBounds(density, bounds))
+        // Ignore if the bounds in portrait
+        bounds = Rect(0, 0, height, width)
+        assertTrue(rule.checkParentBounds(density, bounds))
+
+        // Compare the aspect ratio in landscape
+        rule = SplitPlaceholderRule.Builder(HashSet(), Intent())
+            .setMinWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMinSmallestWidthDp(SPLIT_MIN_DIMENSION_ALWAYS_ALLOW)
+            .setMaxAspectRatioInPortrait(alwaysAllow())
+            .setMaxAspectRatioInLandscape(ratio(1.1f))
+            .build()
+        // Equals to the max aspect ratio
+        width = 110
+        height = 100
+        bounds = Rect(0, 0, width, height)
+        assertTrue(rule.checkParentBounds(density, bounds))
+        // Greater than the max aspect ratio
+        width = 111
+        height = 100
+        bounds = Rect(0, 0, width, height)
+        assertFalse(rule.checkParentBounds(density, bounds))
+        // Ignore if the bounds in portrait
+        bounds = Rect(0, 0, height, width)
+        assertTrue(rule.checkParentBounds(density, bounds))
     }
 
     /**
@@ -264,6 +577,19 @@
     }
 
     /**
+     * Verifies that params are set correctly when reading {@link ActivityRule} from XML.
+     * @see R.xml.test_split_config_custom_activity_rule for customized value.
+     */
+    @Test
+    fun testCustom_ActivityRule_Xml() {
+        val rules = RuleController
+            .parseRules(application, R.xml.test_split_config_custom_activity_rule)
+        assertEquals(1, rules.size)
+        val rule: ActivityRule = rules.first() as ActivityRule
+        assertEquals(true, rule.alwaysExpand)
+    }
+
+    /**
      * Verifies that default params are set correctly when creating {@link ActivityRule} with a
      * builder.
      */
@@ -296,7 +622,7 @@
         // Get the screen's density scale
         val scale: Float = density
         // Convert the dps to pixels, based on density scale
-        val minValidWidthPx = (DEFAULT_SPLIT_MIN_DIMENSION_DP * scale + 0.5f).toInt()
+        val minValidWidthPx = (SPLIT_MIN_DIMENSION_DP_DEFAULT * scale + 0.5f).toInt()
 
         return Rect(0, 0, minValidWidthPx, minValidWidthPx)
     }
@@ -305,7 +631,7 @@
         // Get the screen's density scale
         val scale: Float = density
         // Convert the dps to pixels, based on density scale
-        val minValidWidthPx = ((DEFAULT_SPLIT_MIN_DIMENSION_DP) - 1 * scale + 0.5f).toInt()
+        val minValidWidthPx = ((SPLIT_MIN_DIMENSION_DP_DEFAULT) - 1 * scale + 0.5f).toInt()
 
         return Rect(0, 0, minValidWidthPx, minValidWidthPx)
     }
diff --git a/window/window/src/androidTest/res/xml/test_split_config_custom_activity_rule.xml b/window/window/src/androidTest/res/xml/test_split_config_custom_activity_rule.xml
new file mode 100644
index 0000000..63e54b6
--- /dev/null
+++ b/window/window/src/androidTest/res/xml/test_split_config_custom_activity_rule.xml
@@ -0,0 +1,24 @@
+<?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:window="http://schemas.android.com/apk/res-auto">
+    <ActivityRule
+        window:alwaysExpand="true">
+        <ActivityFilter
+            window:activityName="androidx.window.sample.embedding.SplitActivityList"/>
+    </ActivityRule>
+</resources>
\ No newline at end of file
diff --git a/window/window/src/androidTest/res/xml/test_split_config_custom_split_pair_rule.xml b/window/window/src/androidTest/res/xml/test_split_config_custom_split_pair_rule.xml
new file mode 100644
index 0000000..128eeaa
--- /dev/null
+++ b/window/window/src/androidTest/res/xml/test_split_config_custom_split_pair_rule.xml
@@ -0,0 +1,34 @@
+<?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:window="http://schemas.android.com/apk/res-auto">
+    <SplitPairRule
+        window:splitRatio="0.1"
+        window:splitMinWidthDp="123"
+        window:splitMinSmallestWidthDp="456"
+        window:splitMaxAspectRatioInPortrait="1.23"
+        window:splitMaxAspectRatioInLandscape="alwaysDisallow"
+        window:splitLayoutDirection="rtl"
+        window:finishPrimaryWithSecondary="always"
+        window:finishSecondaryWithPrimary="never"
+        window:clearTop="true"
+        >
+        <SplitPairFilter
+            window:primaryActivityName="A"
+            window:secondaryActivityName="B"/>
+    </SplitPairRule>
+</resources>
\ No newline at end of file
diff --git a/window/window/src/androidTest/res/xml/test_split_config_custom_split_placeholder_rule.xml b/window/window/src/androidTest/res/xml/test_split_config_custom_split_placeholder_rule.xml
new file mode 100644
index 0000000..3a0716e
--- /dev/null
+++ b/window/window/src/androidTest/res/xml/test_split_config_custom_split_placeholder_rule.xml
@@ -0,0 +1,32 @@
+<?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:window="http://schemas.android.com/apk/res-auto">
+    <SplitPlaceholderRule
+        window:splitRatio="0.1"
+        window:splitMinWidthDp="123"
+        window:splitMinSmallestWidthDp="456"
+        window:splitMaxAspectRatioInPortrait="1.23"
+        window:splitMaxAspectRatioInLandscape="alwaysDisallow"
+        window:splitLayoutDirection="rtl"
+        window:finishPrimaryWithPlaceholder="adjacent"
+        window:stickyPlaceholder="true"
+        window:placeholderActivityName="C">
+        <ActivityFilter
+            window:activityName="androidx.window.sample.embedding.SplitActivityList"/>
+    </SplitPlaceholderRule>
+</resources>
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/EmbeddingAspectRatio.kt b/window/window/src/main/java/androidx/window/embedding/EmbeddingAspectRatio.kt
new file mode 100644
index 0000000..643e6fb
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/embedding/EmbeddingAspectRatio.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.embedding
+
+import androidx.annotation.FloatRange
+
+/**
+ * The aspect ratio of the parent window bounds to allow embedding with the rule.
+ *
+ * @see SplitRule.maxAspectRatioInPortrait
+ * @see SplitRule.maxAspectRatioInLandscape
+ */
+class EmbeddingAspectRatio private constructor(
+    /**
+     * The description of this `EmbeddingAspectRatio`.
+     */
+    internal val description: String,
+
+    /**
+     * Aspect ratio, expressed as (longer dimension / shorter dimension) in decimal form, of the
+     * parent window bounds.
+     * It will act as special identifiers for [ALWAYS_ALLOW] and [ALWAYS_DISALLOW].
+     */
+    internal val value: Float
+) {
+    override fun toString() = "EmbeddingAspectRatio($description)"
+
+    override fun equals(other: Any?): Boolean {
+        if (other === this) return true
+        if (other !is EmbeddingAspectRatio) return false
+        return value == other.value && description == other.description
+    }
+
+    override fun hashCode() = description.hashCode() + 31 * value.hashCode()
+
+    companion object {
+        /**
+         * For max aspect ratio, when the aspect ratio is greater than this value, it means to
+         * disallow embedding.
+         *
+         * For min aspect ratio, when the aspect ratio is smaller than this value, it means to
+         * disallow embedding.
+         *
+         * Values smaller than or equal to `1` are invalid.
+         *
+         * @param ratio the aspect ratio.
+         * @return the [EmbeddingAspectRatio] representing the [ratio].
+         *
+         * @see alwaysAllow for always allow embedding.
+         * @see alwaysDisallow for always disallow embedding.
+         */
+        @JvmStatic
+        fun ratio(@FloatRange(from = 1.0, fromInclusive = false) ratio: Float):
+            EmbeddingAspectRatio {
+            require(ratio > 1) { "Ratio must be greater than 1." }
+            return EmbeddingAspectRatio("ratio:$ratio", ratio)
+        }
+
+        private val ALWAYS_ALLOW = EmbeddingAspectRatio("ALWAYS_ALLOW", 0f)
+
+        /**
+         * Gets the special [EmbeddingAspectRatio] to represent it always allows embedding.
+         *
+         * An example use case is to set it on [SplitRule.maxAspectRatioInLandscape] if the app
+         * wants to always allow embedding as split when the parent window is in landscape.
+         */
+        @JvmStatic
+        fun alwaysAllow() = ALWAYS_ALLOW
+
+        private val ALWAYS_DISALLOW = EmbeddingAspectRatio("ALWAYS_DISALLOW", -1f)
+
+        /**
+         * Gets the special [EmbeddingAspectRatio] to represent it always disallows embedding.
+         *
+         * An example use case is to set it on [SplitRule.maxAspectRatioInPortrait] if the app
+         * wants to disallow embedding as split when the parent window is in portrait.
+         */
+        @JvmStatic
+        fun alwaysDisallow() = ALWAYS_DISALLOW
+
+        /**
+         * Returns a [EmbeddingAspectRatio] with the given [value].
+         */
+        internal fun buildAspectRatioFromValue(value: Float): EmbeddingAspectRatio {
+            return when (value) {
+                ALWAYS_ALLOW.value -> {
+                    alwaysAllow()
+                }
+                ALWAYS_DISALLOW.value -> {
+                    alwaysDisallow()
+                }
+                else -> {
+                    ratio(value)
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/RuleParser.kt b/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
index 0c81a18..5d3cea3 100644
--- a/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
+++ b/window/window/src/main/java/androidx/window/embedding/RuleParser.kt
@@ -25,6 +25,7 @@
 import androidx.annotation.XmlRes
 
 import androidx.window.R
+import androidx.window.embedding.EmbeddingAspectRatio.Companion.buildAspectRatioFromValue
 import androidx.window.embedding.SplitRule.Companion.FINISH_ALWAYS
 import androidx.window.embedding.SplitRule.Companion.FINISH_NEVER
 
@@ -126,6 +127,8 @@
         val ratio: Float
         val minWidthDp: Int
         val minSmallestWidthDp: Int
+        val maxAspectRatioInPortrait: Float
+        val maxAspectRatioInLandscape: Float
         val layoutDir: Int
         val finishPrimaryWithSecondary: Int
         val finishSecondaryWithPrimary: Int
@@ -139,11 +142,19 @@
             ratio = getFloat(R.styleable.SplitPairRule_splitRatio, 0.5f)
             minWidthDp = getInteger(
                 R.styleable.SplitPairRule_splitMinWidthDp,
-                SplitRule.DEFAULT_SPLIT_MIN_DIMENSION_DP
+                SplitRule.SPLIT_MIN_DIMENSION_DP_DEFAULT
             )
             minSmallestWidthDp = getInteger(
                 R.styleable.SplitPairRule_splitMinSmallestWidthDp,
-                SplitRule.DEFAULT_SPLIT_MIN_DIMENSION_DP
+                SplitRule.SPLIT_MIN_DIMENSION_DP_DEFAULT
+            )
+            maxAspectRatioInPortrait = getFloat(
+                R.styleable.SplitPairRule_splitMaxAspectRatioInPortrait,
+                SplitRule.SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT.value
+            )
+            maxAspectRatioInLandscape = getFloat(
+                R.styleable.SplitPairRule_splitMaxAspectRatioInLandscape,
+                SplitRule.SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT.value
             )
             layoutDir = getInt(
                 R.styleable.SplitPairRule_splitLayoutDirection,
@@ -159,6 +170,8 @@
         return SplitPairRule.Builder(emptySet())
             .setMinWidthDp(minWidthDp)
             .setMinSmallestWidthDp(minSmallestWidthDp)
+            .setMaxAspectRatioInPortrait(buildAspectRatioFromValue(maxAspectRatioInPortrait))
+            .setMaxAspectRatioInLandscape(buildAspectRatioFromValue(maxAspectRatioInLandscape))
             .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary)
             .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary)
             .setClearTop(clearTop)
@@ -177,6 +190,8 @@
         val ratio: Float
         val minWidthDp: Int
         val minSmallestWidthDp: Int
+        val maxAspectRatioInPortrait: Float
+        val maxAspectRatioInLandscape: Float
         val layoutDir: Int
         context.theme.obtainStyledAttributes(
             parser,
@@ -194,11 +209,19 @@
             ratio = getFloat(R.styleable.SplitPlaceholderRule_splitRatio, 0.5f)
             minWidthDp = getInteger(
                 R.styleable.SplitPlaceholderRule_splitMinWidthDp,
-                SplitRule.DEFAULT_SPLIT_MIN_DIMENSION_DP
+                SplitRule.SPLIT_MIN_DIMENSION_DP_DEFAULT
             )
             minSmallestWidthDp = getInteger(
                 R.styleable.SplitPlaceholderRule_splitMinSmallestWidthDp,
-                SplitRule.DEFAULT_SPLIT_MIN_DIMENSION_DP
+                SplitRule.SPLIT_MIN_DIMENSION_DP_DEFAULT
+            )
+            maxAspectRatioInPortrait = getFloat(
+                R.styleable.SplitPlaceholderRule_splitMaxAspectRatioInPortrait,
+                SplitRule.SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT.value
+            )
+            maxAspectRatioInLandscape = getFloat(
+                R.styleable.SplitPlaceholderRule_splitMaxAspectRatioInLandscape,
+                SplitRule.SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT.value
             )
             layoutDir = getInt(
                 R.styleable.SplitPlaceholderRule_splitLayoutDirection,
@@ -223,6 +246,8 @@
         )
             .setMinWidthDp(minWidthDp)
             .setMinSmallestWidthDp(minSmallestWidthDp)
+            .setMaxAspectRatioInPortrait(buildAspectRatioFromValue(maxAspectRatioInPortrait))
+            .setMaxAspectRatioInLandscape(buildAspectRatioFromValue(maxAspectRatioInLandscape))
             .setSticky(stickyPlaceholder)
             .setFinishPrimaryWithPlaceholder(finishPrimaryWithPlaceholder)
             .setSplitRatio(ratio)
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
index 1969f04..0b684a4 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
@@ -19,8 +19,6 @@
 import android.util.LayoutDirection.LOCALE
 import androidx.annotation.FloatRange
 import androidx.annotation.IntRange
-import androidx.core.util.Preconditions.checkArgument
-import androidx.core.util.Preconditions.checkArgumentNonnegative
 
 /**
  * Split configuration rules for activity pairs. Define when activities that were launched on top of
@@ -64,14 +62,14 @@
         @SplitFinishBehavior finishPrimaryWithSecondary: Int = FINISH_NEVER,
         @SplitFinishBehavior finishSecondaryWithPrimary: Int = FINISH_ALWAYS,
         clearTop: Boolean = false,
-        @IntRange(from = 0) minWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP,
-        @IntRange(from = 0) minSmallestWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP,
-        @FloatRange(from = 0.0, to = 1.0) splitRatio: Float = 0.5f,
+        @IntRange(from = 0) minWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
+        @IntRange(from = 0) minSmallestWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
+        maxAspectRatioInPortrait: EmbeddingAspectRatio = SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT,
+        maxAspectRatioInLandscape: EmbeddingAspectRatio = SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT,
+        @FloatRange(from = 0.0, to = 1.0) splitRatio: Float = SPLIT_RATIO_DEFAULT,
         @LayoutDirection layoutDirection: Int = LOCALE
-    ) : super(minWidthDp, minSmallestWidthDp, splitRatio, layoutDirection) {
-        checkArgumentNonnegative(minWidthDp, "minWidthDp must be non-negative")
-        checkArgumentNonnegative(minSmallestWidthDp, "minSmallestWidthDp must be non-negative")
-        checkArgument(splitRatio in 0.0..1.0, "splitRatio must be in 0.0..1.0 range")
+    ) : super(minWidthDp, minSmallestWidthDp, maxAspectRatioInPortrait, maxAspectRatioInLandscape,
+        splitRatio, layoutDirection) {
         this.filters = filters.toSet()
         this.clearTop = clearTop
         this.finishPrimaryWithSecondary = finishPrimaryWithSecondary
@@ -87,18 +85,20 @@
         private val filters: Set<SplitPairFilter>,
     ) {
         @IntRange(from = 0)
-        private var minWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP
+        private var minWidthDp = SPLIT_MIN_DIMENSION_DP_DEFAULT
         @IntRange(from = 0)
-        private var minSmallestWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP
+        private var minSmallestWidthDp = SPLIT_MIN_DIMENSION_DP_DEFAULT
+        private var maxAspectRatioInPortrait = SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT
+        private var maxAspectRatioInLandscape = SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
         @SplitFinishBehavior
-        private var finishPrimaryWithSecondary: Int = FINISH_NEVER
+        private var finishPrimaryWithSecondary = FINISH_NEVER
         @SplitFinishBehavior
-        private var finishSecondaryWithPrimary: Int = FINISH_ALWAYS
-        private var clearTop: Boolean = false
+        private var finishSecondaryWithPrimary = FINISH_ALWAYS
+        private var clearTop = false
         @FloatRange(from = 0.0, to = 1.0)
-        private var splitRatio: Float = 0.5f
+        private var splitRatio = SPLIT_RATIO_DEFAULT
         @LayoutDirection
-        private var layoutDirection: Int = LOCALE
+        private var layoutDirection = LOCALE
 
         /**
          * @see SplitPairRule.minWidthDp
@@ -113,6 +113,18 @@
             apply { this.minSmallestWidthDp = minSmallestWidthDp }
 
         /**
+         * @see SplitPairRule.maxAspectRatioInPortrait
+         */
+        fun setMaxAspectRatioInPortrait(aspectRatio: EmbeddingAspectRatio): Builder =
+            apply { this.maxAspectRatioInPortrait = aspectRatio }
+
+        /**
+         * @see SplitPairRule.maxAspectRatioInLandscape
+         */
+        fun setMaxAspectRatioInLandscape(aspectRatio: EmbeddingAspectRatio): Builder =
+            apply { this.maxAspectRatioInLandscape = aspectRatio }
+
+        /**
          * @see SplitPairRule.finishPrimaryWithSecondary
          */
         fun setFinishPrimaryWithSecondary(
@@ -148,7 +160,8 @@
             apply { this.layoutDirection = layoutDirection }
 
         fun build() = SplitPairRule(filters, finishPrimaryWithSecondary, finishSecondaryWithPrimary,
-            clearTop, minWidthDp, minSmallestWidthDp, splitRatio, layoutDirection)
+            clearTop, minWidthDp, minSmallestWidthDp, maxAspectRatioInPortrait,
+            maxAspectRatioInLandscape, splitRatio, layoutDirection)
     }
 
     /**
@@ -162,6 +175,8 @@
         return Builder(newSet.toSet())
             .setMinWidthDp(minWidthDp)
             .setMinSmallestWidthDp(minSmallestWidthDp)
+            .setMaxAspectRatioInPortrait(maxAspectRatioInPortrait)
+            .setMaxAspectRatioInLandscape(maxAspectRatioInLandscape)
             .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary)
             .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary)
             .setClearTop(clearTop)
@@ -191,4 +206,18 @@
         result = 31 * result + clearTop.hashCode()
         return result
     }
+
+    override fun toString(): String =
+        "${SplitPairRule::class.java.simpleName}{" +
+            " splitRatio=$splitRatio" +
+            ", layoutDirection=$layoutDirection" +
+            ", minWidthDp=$minWidthDp" +
+            ", minSmallestWidthDp=$minSmallestWidthDp" +
+            ", maxAspectRatioInPortrait=$maxAspectRatioInPortrait" +
+            ", maxAspectRatioInLandscape=$maxAspectRatioInLandscape" +
+            ", clearTop=$clearTop" +
+            ", finishPrimaryWithSecondary=$finishPrimaryWithSecondary" +
+            ", finishSecondaryWithPrimary=$finishSecondaryWithPrimary" +
+            ", filters=$filters" +
+            "}"
 }
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
index 6250e82..fe5f896 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
@@ -22,7 +22,6 @@
 import androidx.annotation.IntDef
 import androidx.annotation.IntRange
 import androidx.core.util.Preconditions.checkArgument
-import androidx.core.util.Preconditions.checkArgumentNonnegative
 
 /**
  * Configuration rules for split placeholders.
@@ -81,14 +80,14 @@
         placeholderIntent: Intent,
         isSticky: Boolean,
         @SplitPlaceholderFinishBehavior finishPrimaryWithPlaceholder: Int = FINISH_ALWAYS,
-        @IntRange(from = 0) minWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP,
-        @IntRange(from = 0) minSmallestWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP,
-        @FloatRange(from = 0.0, to = 1.0) splitRatio: Float = 0.5f,
+        @IntRange(from = 0) minWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
+        @IntRange(from = 0) minSmallestWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
+        maxAspectRatioInPortrait: EmbeddingAspectRatio = SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT,
+        maxAspectRatioInLandscape: EmbeddingAspectRatio = SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT,
+        @FloatRange(from = 0.0, to = 1.0) splitRatio: Float = SPLIT_RATIO_DEFAULT,
         @LayoutDirection layoutDirection: Int = LOCALE
-    ) : super(minWidthDp, minSmallestWidthDp, splitRatio, layoutDirection) {
-        checkArgumentNonnegative(minWidthDp, "minWidthDp must be non-negative")
-        checkArgumentNonnegative(minSmallestWidthDp, "minSmallestWidthDp must be non-negative")
-        checkArgument(splitRatio in 0.0..1.0, "splitRatio must be in 0.0..1.0 range")
+    ) : super(minWidthDp, minSmallestWidthDp, maxAspectRatioInPortrait, maxAspectRatioInLandscape,
+        splitRatio, layoutDirection) {
         checkArgument(finishPrimaryWithPlaceholder != FINISH_NEVER,
             "FINISH_NEVER is not a valid configuration for SplitPlaceholderRule. " +
                 "Please use FINISH_ALWAYS or FINISH_ADJACENT instead or refer to the current API.")
@@ -109,16 +108,18 @@
         private val placeholderIntent: Intent,
     ) {
         @IntRange(from = 0)
-        private var minWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP
+        private var minWidthDp = SPLIT_MIN_DIMENSION_DP_DEFAULT
         @IntRange(from = 0)
-        private var minSmallestWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP
+        private var minSmallestWidthDp = SPLIT_MIN_DIMENSION_DP_DEFAULT
+        private var maxAspectRatioInPortrait = SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT
+        private var maxAspectRatioInLandscape = SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT
         @SplitPlaceholderFinishBehavior
-        private var finishPrimaryWithPlaceholder: Int = FINISH_ALWAYS
-        private var isSticky: Boolean = false
+        private var finishPrimaryWithPlaceholder = FINISH_ALWAYS
+        private var isSticky = false
         @FloatRange(from = 0.0, to = 1.0)
-        private var splitRatio: Float = 0.5f
+        private var splitRatio = SPLIT_RATIO_DEFAULT
         @LayoutDirection
-        private var layoutDirection: Int = LOCALE
+        private var layoutDirection = LOCALE
 
         /**
          * @see SplitPlaceholderRule.minWidthDp
@@ -133,6 +134,18 @@
             apply { this.minSmallestWidthDp = minSmallestWidthDp }
 
         /**
+         * @see SplitPlaceholderRule.maxAspectRatioInPortrait
+         */
+        fun setMaxAspectRatioInPortrait(aspectRatio: EmbeddingAspectRatio): Builder =
+            apply { this.maxAspectRatioInPortrait = aspectRatio }
+
+        /**
+         * @see SplitPlaceholderRule.maxAspectRatioInLandscape
+         */
+        fun setMaxAspectRatioInLandscape(aspectRatio: EmbeddingAspectRatio): Builder =
+            apply { this.maxAspectRatioInLandscape = aspectRatio }
+
+        /**
          * @see SplitPlaceholderRule.finishPrimaryWithPlaceholder
          */
         fun setFinishPrimaryWithPlaceholder(
@@ -161,8 +174,8 @@
             apply { this.layoutDirection = layoutDirection }
 
         fun build() = SplitPlaceholderRule(filters, placeholderIntent, isSticky,
-            finishPrimaryWithPlaceholder, minWidthDp, minSmallestWidthDp, splitRatio,
-            layoutDirection)
+            finishPrimaryWithPlaceholder, minWidthDp, minSmallestWidthDp, maxAspectRatioInPortrait,
+            maxAspectRatioInLandscape, splitRatio, layoutDirection)
     }
 
     /**
@@ -176,6 +189,8 @@
         return Builder(newSet.toSet(), placeholderIntent)
             .setMinWidthDp(minWidthDp)
             .setMinSmallestWidthDp(minSmallestWidthDp)
+            .setMaxAspectRatioInPortrait(maxAspectRatioInPortrait)
+            .setMaxAspectRatioInLandscape(maxAspectRatioInLandscape)
             .setSticky(isSticky)
             .setFinishPrimaryWithPlaceholder(finishPrimaryWithPlaceholder)
             .setSplitRatio(splitRatio)
@@ -204,4 +219,18 @@
         result = 31 * result + filters.hashCode()
         return result
     }
+
+    override fun toString(): String =
+        "SplitPlaceholderRule{" +
+            " splitRatio=$splitRatio" +
+            ", layoutDirection=$layoutDirection" +
+            ", minWidthDp=$minWidthDp" +
+            ", minSmallestWidthDp=$minSmallestWidthDp" +
+            ", maxAspectRatioInPortrait=$maxAspectRatioInPortrait" +
+            ", maxAspectRatioInLandscape=$maxAspectRatioInLandscape" +
+            ", placeholderIntent=$placeholderIntent" +
+            ", isSticky=$isSticky" +
+            ", finishPrimaryWithPlaceholder=$finishPrimaryWithPlaceholder" +
+            ", filters=$filters" +
+            "}"
 }
\ No newline at end of file
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
index eb773d3..c4f7905 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
@@ -28,7 +28,10 @@
 import androidx.annotation.IntDef
 import androidx.annotation.IntRange
 import androidx.annotation.RequiresApi
-import androidx.window.embedding.SplitRule.Companion.DEFAULT_SPLIT_MIN_DIMENSION_DP
+import androidx.core.util.Preconditions
+import androidx.window.embedding.EmbeddingAspectRatio.Companion.alwaysAllow
+import androidx.window.embedding.EmbeddingAspectRatio.Companion.ratio
+import androidx.window.embedding.SplitRule.Companion.SPLIT_MIN_DIMENSION_DP_DEFAULT
 import kotlin.math.min
 
 /**
@@ -46,11 +49,11 @@
      * When the window size is smaller than requested here, activities in the secondary container
      * will be stacked on top of the activities in the primary one, completely overlapping them.
      *
-     * The default is [DEFAULT_SPLIT_MIN_DIMENSION_DP] if the app doesn't set.
-     * `0` means to always allow split.
+     * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
+     * [SPLIT_MIN_DIMENSION_ALWAYS_ALLOW] means to always allow split.
      */
     @IntRange(from = 0)
-    val minWidthDp: Int = DEFAULT_SPLIT_MIN_DIMENSION_DP,
+    val minWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
 
     /**
      * The smallest value of the smallest possible width of the parent window in any rotation
@@ -58,18 +61,53 @@
      * here, activities in the secondary container will be stacked on top of the activities in
      * the primary one, completely overlapping them.
      *
-     * The default is [DEFAULT_SPLIT_MIN_DIMENSION_DP] if the app doesn't set.
-     * `0` means to always allow split.
+     * The default is [SPLIT_MIN_DIMENSION_DP_DEFAULT] if the app doesn't set.
+     * [SPLIT_MIN_DIMENSION_ALWAYS_ALLOW] means to always allow split.
      */
     @IntRange(from = 0)
-    val minSmallestWidthDp: Int,
+    val minSmallestWidthDp: Int = SPLIT_MIN_DIMENSION_DP_DEFAULT,
 
     /**
-     * Defines what part of the width should be given to the primary activity. Defaults to an
-     * equal width split.
+     * The largest value of the aspect ratio, expressed as (height / width) in decimal form, of the
+     * parent window bounds in portrait when the split should be used. When the window aspect ratio
+     * is greater than requested here, activities in the secondary container will stacked on top of
+     * the activities in the primary one, completely overlapping them.
+     *
+     * This value is only used when the parent window is in portrait (height >= width).
+     *
+     * The default is [SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT] if the app doesn't set, which is the
+     * recommend value to only allow split when the parent window is not too stretched in portrait.
+     *
+     * @see EmbeddingAspectRatio.ratio
+     * @see EmbeddingAspectRatio.alwaysAllow
+     * @see EmbeddingAspectRatio.alwaysDisallow
+     */
+    val maxAspectRatioInPortrait: EmbeddingAspectRatio = SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT,
+
+    /**
+     * The largest value of the aspect ratio, expressed as (width / height) in decimal form, of the
+     * parent window bounds in landscape when the split should be used. When the window aspect ratio
+     * is greater than requested here, activities in the secondary container will stacked on top of
+     * the activities in the primary one, completely overlapping them.
+     *
+     * This value is only used when the parent window is in landscape (width > height).
+     *
+     * The default is [SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT] if the app doesn't set, which is
+     * the recommend value to always allow split when the parent window is in landscape.
+     *
+     * @see EmbeddingAspectRatio.ratio
+     * @see EmbeddingAspectRatio.alwaysAllow
+     * @see EmbeddingAspectRatio.alwaysDisallow
+     */
+    val maxAspectRatioInLandscape: EmbeddingAspectRatio = SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT,
+
+    /**
+     * Defines what part of the width should be given to the primary activity.
+     *
+     * The default is `0.5` if the app doesn't set, which is to split with equal width.
      */
     @FloatRange(from = 0.0, to = 1.0)
-    val splitRatio: Float = 0.5f,
+    val splitRatio: Float = SPLIT_RATIO_DEFAULT,
 
     /**
      * The layout direction for the split. The value must be one of [LTR], [RTL] or [LOCALE].
@@ -84,6 +122,15 @@
     val layoutDirection: Int = LOCALE
 ) : EmbeddingRule() {
 
+    init {
+        Preconditions.checkArgumentNonnegative(minWidthDp, "minWidthDp must be non-negative")
+        Preconditions.checkArgumentNonnegative(
+            minSmallestWidthDp,
+            "minSmallestWidthDp must be non-negative"
+        )
+        Preconditions.checkArgument(splitRatio in 0.0..1.0, "splitRatio must be in 0.0..1.0 range")
+    }
+
     @IntDef(LTR, RTL, LOCALE)
     @Retention(AnnotationRetention.SOURCE)
     internal annotation class LayoutDirection
@@ -119,11 +166,41 @@
          * @see SplitRule.Companion
          */
         const val FINISH_ADJACENT = 2
+
+        /**
+         * The default split ratio if it is not set by apps.
+         * @see SplitRule.splitRatio
+         */
+        internal const val SPLIT_RATIO_DEFAULT = 0.5f
+
+        /**
+         * When the min dimension is set to this value, it means to always allow split.
+         * @see SplitRule.minWidthDp
+         * @see SplitRule.minSmallestWidthDp
+         */
+        const val SPLIT_MIN_DIMENSION_ALWAYS_ALLOW = 0
+
         /**
          * The default min dimension in DP for allowing split if it is not set by apps. The value
          * reflects [androidx.window.core.layout.WindowWidthSizeClass.MEDIUM].
+         * @see SplitRule.minWidthDp
+         * @see SplitRule.minSmallestWidthDp
          */
-        const val DEFAULT_SPLIT_MIN_DIMENSION_DP = 600
+        const val SPLIT_MIN_DIMENSION_DP_DEFAULT = 600
+
+        /**
+         * The default max aspect ratio for allowing split when the parent window is in portrait.
+         * @see SplitRule.maxAspectRatioInPortrait
+         */
+        @JvmField
+        val SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT = ratio(1.4f)
+
+        /**
+         * The default max aspect ratio for allowing split when the parent window is in landscape.
+         * @see SplitRule.maxAspectRatioInLandscape
+         */
+        @JvmField
+        val SPLIT_MAX_ASPECT_RATIO_LANDSCAPE_DEFAULT = alwaysAllow()
     }
 
     /**
@@ -135,7 +212,8 @@
     internal annotation class SplitFinishBehavior
 
     /**
-     * Verifies if the provided parent bounds are large enough to apply the rule.
+     * Verifies if the provided parent bounds satisfy the dimensions and aspect ratio requirements
+     * to apply the rule.
      */
     internal fun checkParentMetrics(context: Context, parentMetrics: WindowMetrics): Boolean {
         if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
@@ -152,14 +230,27 @@
      * @see checkParentMetrics
      */
     internal fun checkParentBounds(density: Float, bounds: Rect): Boolean {
+        val width = bounds.width()
+        val height = bounds.height()
+        if (width == 0 || height == 0) {
+            return false
+        }
         val minWidthPx = convertDpToPx(density, minWidthDp)
         val minSmallestWidthPx = convertDpToPx(density, minSmallestWidthDp)
-        val validMinWidth = (minWidthDp == 0 || bounds.width() >= minWidthPx)
-        val validSmallestMinWidth = (
-            minSmallestWidthDp == 0 ||
-                min(bounds.width(), bounds.height()) >= minSmallestWidthPx
-            )
-        return validMinWidth && validSmallestMinWidth
+        // Always allow split if the min dimensions are 0.
+        val validMinWidth = minWidthDp == SPLIT_MIN_DIMENSION_ALWAYS_ALLOW || width >= minWidthPx
+        val validSmallestMinWidth = minSmallestWidthDp == SPLIT_MIN_DIMENSION_ALWAYS_ALLOW ||
+            min(width, height) >= minSmallestWidthPx
+        val validAspectRatio = if (height >= width) {
+            // Portrait
+            maxAspectRatioInPortrait == alwaysAllow() ||
+                height * 1f / width <= maxAspectRatioInPortrait.value
+        } else {
+            // Landscape
+            maxAspectRatioInLandscape == alwaysAllow() ||
+                width * 1f / height <= maxAspectRatioInLandscape.value
+        }
+        return validMinWidth && validSmallestMinWidth && validAspectRatio
     }
 
     /**
@@ -183,6 +274,8 @@
 
         if (minWidthDp != other.minWidthDp) return false
         if (minSmallestWidthDp != other.minSmallestWidthDp) return false
+        if (maxAspectRatioInPortrait != other.maxAspectRatioInPortrait) return false
+        if (maxAspectRatioInLandscape != other.maxAspectRatioInLandscape) return false
         if (splitRatio != other.splitRatio) return false
         if (layoutDirection != other.layoutDirection) return false
 
@@ -192,8 +285,20 @@
     override fun hashCode(): Int {
         var result = minWidthDp
         result = 31 * result + minSmallestWidthDp
+        result = 31 * result + maxAspectRatioInPortrait.hashCode()
+        result = 31 * result + maxAspectRatioInLandscape.hashCode()
         result = 31 * result + splitRatio.hashCode()
         result = 31 * result + layoutDirection
         return result
     }
+
+    override fun toString(): String =
+        "${SplitRule::class.java.simpleName}{" +
+            " splitRatio=$splitRatio" +
+            ", layoutDirection=$layoutDirection" +
+            ", minWidthDp=$minWidthDp" +
+            ", minSmallestWidthDp=$minSmallestWidthDp" +
+            ", maxAspectRatioInPortrait=$maxAspectRatioInPortrait" +
+            ", maxAspectRatioInLandscape=$maxAspectRatioInLandscape" +
+            "}"
 }
\ No newline at end of file
diff --git a/window/window/src/main/res/values/attrs.xml b/window/window/src/main/res/values/attrs.xml
index 612d1f2..96527f8 100644
--- a/window/window/src/main/res/values/attrs.xml
+++ b/window/window/src/main/res/values/attrs.xml
@@ -21,8 +21,30 @@
     <!-- The smallest value of width of the parent window when the split should be used. -->
     <attr name="splitMinWidthDp" format="integer" />
     <!-- The smallest value of the smallest-width (sw) of the parent window in any rotation when
-     the split should be used. -->
+    the split should be used. -->
     <attr name="splitMinSmallestWidthDp" format="integer" />
+    <!-- The largest value of the aspect ratio, expressed as (height / width) in decimal form, of
+    the parent window bounds in portrait when the split should be used.
+    `0` or `alwaysAllow` means to always allow split in portrait.
+    `-1` or `alwaysDisallow` means to always disallow split in portrait.
+    Any other values less than 1 are invalid. -->
+    <attr name="splitMaxAspectRatioInPortrait"  format="float">
+        <!-- Special value to always allow split in portrait. -->
+        <enum name="alwaysAllow" value="0" />
+        <!-- Special value to always disallow split in portrait. -->
+        <enum name="alwaysDisallow" value="-1" />
+    </attr>
+    <!-- The largest value of the aspect ratio, expressed as (width / height) in decimal form, of
+    the parent window bounds in landscape when the split should be used.
+    `0` or `alwaysAllow` means to always allow split in landscape.
+    `-1` or `alwaysDisallow` means to always disallow split in landscape.
+    Any other values less than 1 are invalid. -->
+    <attr name="splitMaxAspectRatioInLandscape"  format="float">
+        <!-- Special value to always allow split in landscape. -->
+        <enum name="alwaysAllow" value="0" />
+        <!-- Special value to always disallow split in landscape. -->
+        <enum name="alwaysDisallow" value="-1" />
+    </attr>
     <!-- The layout direction for the split. The value must be one of "ltr", "rtl" or "locale". -->
     <attr name="splitLayoutDirection" format="enum">
         <!-- It splits the task bounds vertically, and the direction is deduced from the default
@@ -67,6 +89,8 @@
         <attr name="splitRatio"/>
         <attr name="splitMinWidthDp"/>
         <attr name="splitMinSmallestWidthDp"/>
+        <attr name="splitMaxAspectRatioInPortrait" />
+        <attr name="splitMaxAspectRatioInLandscape" />
         <attr name="splitLayoutDirection"/>
     </declare-styleable>
 
@@ -85,6 +109,8 @@
         <attr name="splitRatio"/>
         <attr name="splitMinWidthDp"/>
         <attr name="splitMinSmallestWidthDp"/>
+        <attr name="splitMaxAspectRatioInPortrait" />
+        <attr name="splitMaxAspectRatioInLandscape" />
         <attr name="splitLayoutDirection"/>
     </declare-styleable>
 
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/src/main/java/androidx/work/inspection/WorkManagerInspector.kt b/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt
index 66a0dd2..f51e4cf 100644
--- a/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt
+++ b/work/work-inspection/src/main/java/androidx/work/inspection/WorkManagerInspector.kt
@@ -133,13 +133,13 @@
                 owner,
                 object : Observer<T> {
                     private var lastValue: T? = null
-                    override fun onChanged(t: T) {
-                        if (t == null) {
+                    override fun onChanged(value: T) {
+                        if (value == null) {
                             removeObserver(this)
                         } else {
                             executor.execute {
-                                listener(lastValue, t)
-                                lastValue = t
+                                listener(lastValue, value)
+                                lastValue = value
                             }
                         }
                     }
diff --git a/work/work-runtime/src/androidTest/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();