Merge "Properly set JavaCompile jvmTarget for KMP libraries using withJava" into androidx-main
diff --git a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
index 9fc459b..a5dda1a 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
@@ -18,7 +18,7 @@
 
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.testing.TestLifecycleOwner
-import androidx.navigationevent.DirectNavigationEventInputHandler
+import androidx.navigationevent.DirectNavigationEventInput
 import androidx.navigationevent.testing.TestNavigationEventCallback
 import androidx.test.annotation.UiThreadTest
 import androidx.test.core.app.ActivityScenario
@@ -573,9 +573,9 @@
         val callback = CountingOnBackPressedCallback()
         dispatcher.addCallback(callback)
 
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.eventDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnCompleted()
+        val input = DirectNavigationEventInput()
+        dispatcher.eventDispatcher.addInput(input)
+        input.handleOnCompleted()
 
         assertWithMessage("Count should be incremented after dispatchOnCompleted")
             .that(callback.count)
diff --git a/activity/activity/src/main/java/androidx/activity/ComponentActivity.kt b/activity/activity/src/main/java/androidx/activity/ComponentActivity.kt
index dd1a301..4eb5c69 100644
--- a/activity/activity/src/main/java/androidx/activity/ComponentActivity.kt
+++ b/activity/activity/src/main/java/androidx/activity/ComponentActivity.kt
@@ -88,7 +88,7 @@
 import androidx.lifecycle.setViewTreeViewModelStoreOwner
 import androidx.lifecycle.viewmodel.CreationExtras
 import androidx.lifecycle.viewmodel.MutableCreationExtras
-import androidx.navigationevent.DirectNavigationEventInputHandler
+import androidx.navigationevent.DirectNavigationEventInput
 import androidx.navigationevent.NavigationEventDispatcher
 import androidx.navigationevent.NavigationEventDispatcherOwner
 import androidx.navigationevent.setViewTreeNavigationEventDispatcherOwner
@@ -241,12 +241,12 @@
     private var dispatchingOnMultiWindowModeChanged = false
     private var dispatchingOnPictureInPictureModeChanged = false
 
-    // Input from `ComponentActivity.onBackPressed()`, which can get called when API < 33 or
+    // Inputs from `ComponentActivity.onBackPressed()`, which can get called when API < 33 or
     // when `android:enableOnBackInvokedCallback` is `false`.
-    private val onBackPressedInputHandler: DirectNavigationEventInputHandler by lazy {
-        val inputHandler = DirectNavigationEventInputHandler()
-        navigationEventDispatcher.addInputHandler(inputHandler)
-        inputHandler
+    private val onBackPressedInput: DirectNavigationEventInput by lazy {
+        val input = DirectNavigationEventInput()
+        navigationEventDispatcher.addInput(input)
+        input
     }
 
     /**
@@ -595,7 +595,7 @@
       to one or more {@link OnBackPressedCallback} objects."""
     )
     override fun onBackPressed() {
-        onBackPressedInputHandler.handleOnCompleted()
+        onBackPressedInput.handleOnCompleted()
     }
 
     /**
diff --git a/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt b/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt
index 5ecc60d..6ab4f9b 100644
--- a/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt
+++ b/activity/activity/src/main/java/androidx/activity/ComponentDialog.kt
@@ -28,7 +28,7 @@
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LifecycleRegistry
 import androidx.lifecycle.setViewTreeLifecycleOwner
-import androidx.navigationevent.DirectNavigationEventInputHandler
+import androidx.navigationevent.DirectNavigationEventInput
 import androidx.navigationevent.NavigationEventDispatcher
 import androidx.navigationevent.NavigationEventDispatcherOwner
 import androidx.navigationevent.setViewTreeNavigationEventDispatcherOwner
@@ -61,10 +61,10 @@
 
     // Input from for `ComponentDialog.onBackPressed()`, which can get called when API < 33 or
     // when `android:enableOnBackInvokedCallback` is `false`.
-    private val onBackPressedInputHandler: DirectNavigationEventInputHandler by lazy {
-        val inputHandler = DirectNavigationEventInputHandler()
-        navigationEventDispatcher.addInputHandler(inputHandler)
-        inputHandler
+    private val onBackPressedInput: DirectNavigationEventInput by lazy {
+        val input = DirectNavigationEventInput()
+        navigationEventDispatcher.addInput(input)
+        input
     }
 
     override fun onSaveInstanceState(): Bundle {
@@ -125,7 +125,7 @@
       to one or more {@link OnBackPressedCallback} objects."""
     )
     override fun onBackPressed() {
-        onBackPressedInputHandler.handleOnCompleted()
+        onBackPressedInput.handleOnCompleted()
     }
 
     override fun setContentView(layoutResID: Int) {
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
index 4b217af..483b56a 100644
--- a/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
+++ b/activity/activity/src/main/java/androidx/activity/OnBackPressedDispatcher.kt
@@ -24,11 +24,11 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
 import androidx.lifecycle.LifecycleOwner
-import androidx.navigationevent.DirectNavigationEventInputHandler
+import androidx.navigationevent.DirectNavigationEventInput
 import androidx.navigationevent.NavigationEventCallback
 import androidx.navigationevent.NavigationEventDispatcher
-import androidx.navigationevent.NavigationEventInputHandler
-import androidx.navigationevent.OnBackInvokedInputHandler
+import androidx.navigationevent.NavigationEventInput
+import androidx.navigationevent.OnBackInvokedInput
 
 /**
  * Dispatcher that can be used to register [OnBackPressedCallback] instances for handling the
@@ -79,8 +79,8 @@
         // This is to implement `OnBackPressedDispatcher.onHasEnabledCallbacksChanged`, which
         // can be set through OnBackPressedDispatcher's public constructor.
         onHasEnabledCallbacksChanged?.let { callback ->
-            dispatcher.addInputHandler(
-                object : NavigationEventInputHandler() {
+            dispatcher.addInput(
+                object : NavigationEventInput() {
                     override fun onHasEnabledCallbacksChanged(hasEnabledCallbacks: Boolean) {
                         callback.accept(hasEnabledCallbacks)
                     }
@@ -90,10 +90,10 @@
         dispatcher
     }
 
-    private val manualDispatchInputHandler by lazy {
-        val inputHandler = DirectNavigationEventInputHandler()
-        eventDispatcher.addInputHandler(inputHandler)
-        inputHandler
+    private val directInput by lazy {
+        val input = DirectNavigationEventInput()
+        eventDispatcher.addInput(input)
+        input
     }
 
     @JvmOverloads
@@ -106,8 +106,8 @@
      */
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
     fun setOnBackInvokedDispatcher(invoker: OnBackInvokedDispatcher) {
-        val inputHandler = OnBackInvokedInputHandler(invoker)
-        eventDispatcher.addInputHandler(inputHandler)
+        val input = OnBackInvokedInput(invoker)
+        eventDispatcher.addInput(input)
     }
 
     /**
@@ -214,13 +214,13 @@
     @VisibleForTesting
     @MainThread
     fun dispatchOnBackStarted(backEvent: BackEventCompat) {
-        manualDispatchInputHandler.handleOnStarted(backEvent.toNavigationEvent())
+        directInput.handleOnStarted(backEvent.toNavigationEvent())
     }
 
     @VisibleForTesting
     @MainThread
     fun dispatchOnBackProgressed(backEvent: BackEventCompat) {
-        manualDispatchInputHandler.handleOnProgressed(backEvent.toNavigationEvent())
+        directInput.handleOnProgressed(backEvent.toNavigationEvent())
     }
 
     /**
@@ -233,13 +233,13 @@
      */
     @MainThread
     fun onBackPressed() {
-        manualDispatchInputHandler.handleOnCompleted()
+        directInput.handleOnCompleted()
     }
 
     @VisibleForTesting
     @MainThread
     fun dispatchOnBackCancelled() {
-        manualDispatchInputHandler.handleOnCancelled()
+        directInput.handleOnCancelled()
     }
 }
 
diff --git a/annotation/annotation/build.gradle b/annotation/annotation/build.gradle
index 2f3c84a..9b0c083 100644
--- a/annotation/annotation/build.gradle
+++ b/annotation/annotation/build.gradle
@@ -39,25 +39,9 @@
             dependsOn(commonMain)
         }
 
-        wasmJsMain {
-            dependsOn(nonJvmMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        jsMain {
-            dependsOn(nonJvmMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
         targets.configureEach { target ->
             if (target.platformType !in [KotlinPlatformType.jvm, KotlinPlatformType.common]) {
-                target.compilations["main"].defaultSourceSet {
-                    dependsOn(nonJvmMain)
-                }
+                target.compilations["main"].defaultSourceSet.dependsOn(nonJvmMain)
             }
         }
 
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializable.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializable.kt
index dcf6837..6dddf79 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializable.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializable.kt
@@ -35,6 +35,8 @@
 import com.squareup.kotlinpoet.ClassName
 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
 import com.squareup.kotlinpoet.TypeName
+import kotlin.String
+import kotlin.text.ifEmpty
 
 // TODO(b/410764334): Re-evaluate the abstraction layer.
 /** Represents a class annotated with [androidx.appfunctions.AppFunctionSerializable]. */
@@ -110,8 +112,8 @@
         ) ?: false
     }
 
-    /** A description of the AppFunctionSerializable class and its intended use. */
-    val description: String by lazy {
+    /** The docstring of the annotated class. */
+    internal val docstring: String by lazy {
         if (isDescribedByKdoc) {
             appFunctionSerializableClass.docString.orEmpty()
         } else {
@@ -119,6 +121,11 @@
         }
     }
 
+    /** A description of the AppFunctionSerializable class and its intended use. */
+    open fun getDescription(sharedDataTypeDescriptionMap: Map<String, String> = mapOf()): String {
+        return docstring.ifEmpty { sharedDataTypeDescriptionMap[jvmQualifiedName] ?: "" }
+    }
+
     /** All the [KSDeclaration] from the AppFunctionSerializable. */
     val declarations: Sequence<KSDeclaration> by lazy { appFunctionSerializableClass.declarations }
 
@@ -255,7 +262,9 @@
     }
 
     /** Returns the annotated class's properties as defined in its primary constructor. */
-    open fun getProperties(): List<AppFunctionPropertyDeclaration> {
+    open fun getProperties(
+        sharedDataTypeDescriptionMap: Map<String, String> = emptyMap()
+    ): List<AppFunctionPropertyDeclaration> {
         val primaryConstructorProperties =
             checkNotNull(appFunctionSerializableClass.primaryConstructor).parameters
 
@@ -267,9 +276,10 @@
         return primaryConstructorProperties.mapNotNull { valueParameter ->
             allProperties[valueParameter.name?.asString()]?.let {
                 AppFunctionPropertyDeclaration(
-                    it,
-                    isDescribedByKdoc,
+                    property = it,
+                    isDescribedByKdoc = isDescribedByKdoc,
                     isRequired = !valueParameter.hasDefault,
+                    sharedDataTypeDescriptionMap = sharedDataTypeDescriptionMap,
                 )
             }
         }
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializableInterface.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializableInterface.kt
index 6ea5de1..0b614a0 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializableInterface.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedAppFunctionSerializableInterface.kt
@@ -30,7 +30,9 @@
         return this
     }
 
-    override fun getProperties(): List<AppFunctionPropertyDeclaration> {
+    override fun getProperties(
+        sharedDataTypeDescriptionMap: Map<String, String>
+    ): List<AppFunctionPropertyDeclaration> {
         return classDeclaration
             .getAllProperties()
             .map {
@@ -40,6 +42,7 @@
                     // Property from interface is always required as there is no existing API
                     // to tell if the interface property has default value or not.
                     isRequired = true,
+                    sharedDataTypeDescriptionMap,
                 )
             }
             .toList()
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedParameterizedAppFunctionSerializable.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedParameterizedAppFunctionSerializable.kt
index da771c6..51711e3 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedParameterizedAppFunctionSerializable.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AnnotatedParameterizedAppFunctionSerializable.kt
@@ -76,7 +76,7 @@
      * The JVM qualified name of the parametrized class being annotated with
      * AppFunctionSerializable, without the parameterized type information
      */
-    val unparameterizedJvmQualifiedName: String by lazy { super.jvmQualifiedName }
+    private val unparameterizedJvmQualifiedName: String by lazy { super.jvmQualifiedName }
 
     override val factoryVariableName: String by lazy {
         val variableName = jvmClassName.replace("$", "").replaceFirstChar { it -> it.lowercase() }
@@ -92,14 +92,22 @@
         "${variableName}${typeArgumentSuffix}Factory"
     }
 
+    override fun getDescription(sharedDataTypeDescriptionMap: Map<String, String>): String {
+        return docstring.ifEmpty {
+            sharedDataTypeDescriptionMap[unparameterizedJvmQualifiedName] ?: ""
+        }
+    }
+
     /**
      * Returns the annotated class's properties as defined in its primary constructor.
      *
      * When the property is generic type, it will try to resolve the actual type reference from
      * [arguments].
      */
-    override fun getProperties(): List<AppFunctionPropertyDeclaration> {
-        return super.getProperties().map { propertyDeclaration ->
+    override fun getProperties(
+        sharedDataTypeDescriptionMap: Map<String, String>
+    ): List<AppFunctionPropertyDeclaration> {
+        return super.getProperties(sharedDataTypeDescriptionMap).map { propertyDeclaration ->
             val valueTypeDeclaration = propertyDeclaration.type.resolve().declaration
             if (valueTypeDeclaration is KSTypeParameter) {
                 val actualType =
@@ -114,6 +122,7 @@
                     description = propertyDeclaration.description,
                     isRequired = propertyDeclaration.isRequired,
                     propertyAnnotations = propertyDeclaration.propertyAnnotations,
+                    qualifiedName = propertyDeclaration.qualifiedName,
                 )
             } else {
                 propertyDeclaration
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionMetadataCreatorHelper.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionMetadataCreatorHelper.kt
index 483ccf3..f840b4c 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionMetadataCreatorHelper.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionMetadataCreatorHelper.kt
@@ -270,7 +270,7 @@
                 addSerializableTypeMetadataToSharedDataTypeMap(
                     annotatedAppFunctionSerializable,
                     annotatedAppFunctionSerializable
-                        .getProperties()
+                        .getProperties(sharedDataTypeDescriptionMap)
                         .associateBy { checkNotNull(it.name).toString() }
                         .toMutableMap(),
                     sharedDataTypeMap,
@@ -294,7 +294,7 @@
                 addSerializableTypeMetadataToSharedDataTypeMap(
                     annotatedAppFunctionSerializable,
                     annotatedAppFunctionSerializable
-                        .getProperties()
+                        .getProperties(sharedDataTypeDescriptionMap)
                         .associateBy { checkNotNull(it.name).toString() }
                         .toMutableMap(),
                     sharedDataTypeMap,
@@ -323,7 +323,7 @@
                 addSerializableTypeMetadataToSharedDataTypeMap(
                     targetSerializableProxy,
                     targetSerializableProxy
-                        .getProperties()
+                        .getProperties(sharedDataTypeDescriptionMap)
                         .associateBy { checkNotNull(it.name).toString() }
                         .toMutableMap(),
                     sharedDataTypeMap,
@@ -349,7 +349,7 @@
                 addSerializableTypeMetadataToSharedDataTypeMap(
                     targetSerializableProxy,
                     targetSerializableProxy
-                        .getProperties()
+                        .getProperties(sharedDataTypeDescriptionMap)
                         .associateBy { checkNotNull(it.name).toString() }
                         .toMutableMap(),
                     sharedDataTypeMap,
@@ -419,14 +419,7 @@
         seenDataTypeQualifiers.add(serializableTypeQualifiedName)
 
         val serializableDescription =
-            when {
-                appFunctionSerializableType.description.isNotEmpty() ->
-                    appFunctionSerializableType.description
-                appFunctionSerializableType is AnnotatedParameterizedAppFunctionSerializable ->
-                    sharedDataTypeDescriptionMap[
-                        appFunctionSerializableType.unparameterizedJvmQualifiedName] ?: ""
-                else -> sharedDataTypeDescriptionMap[serializableTypeQualifiedName] ?: ""
-            }
+            appFunctionSerializableType.getDescription(sharedDataTypeDescriptionMap)
 
         val superTypesWithSerializableAnnotation =
             appFunctionSerializableType.findSuperTypesWithSerializableAnnotation()
@@ -441,7 +434,7 @@
                 serializableTypeQualifiedName,
                 buildObjectTypeMetadataForObjectParameters(
                     serializableTypeQualifiedName,
-                    appFunctionSerializableType.getProperties(),
+                    appFunctionSerializableType.getProperties(sharedDataTypeDescriptionMap),
                     unvisitedSerializableProperties,
                     sharedDataTypeMap,
                     seenDataTypeQualifiers,
@@ -492,6 +485,7 @@
                                         // no existing API to tell if the interface property has
                                         // default value or not.
                                         isRequired = true,
+                                        sharedDataTypeDescriptionMap,
                                     )
                                 }
                                 .toList(),
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionPropertyDeclaration.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionPropertyDeclaration.kt
index 7857a16..7ed061c 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionPropertyDeclaration.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/AppFunctionPropertyDeclaration.kt
@@ -29,22 +29,27 @@
     val description: String,
     val isRequired: Boolean,
     val propertyAnnotations: Sequence<KSAnnotation> = emptySequence(),
+    val qualifiedName: String,
 ) {
     /** Creates an [AppFunctionPropertyDeclaration] from [KSPropertyDeclaration]. */
     constructor(
         property: KSPropertyDeclaration,
         isDescribedByKdoc: Boolean,
         isRequired: Boolean,
+        sharedDataTypeDescriptionMap: Map<String, String>,
     ) : this(
         checkNotNull(property.simpleName).asString(),
         property.type,
         if (isDescribedByKdoc) {
-            property.docString.orEmpty()
+            property.docString?.ifEmpty {
+                sharedDataTypeDescriptionMap[property.getQualifiedName()]
+            } ?: ""
         } else {
             ""
         },
         isRequired,
         property.annotations,
+        property.getQualifiedName(),
     )
 
     /** Indicates whether the [type] is a generic type or not. */
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt
index 87a83c68..9993a166c 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/KspUtils.kt
@@ -21,6 +21,7 @@
 import com.google.devtools.ksp.symbol.KSDeclaration
 import com.google.devtools.ksp.symbol.KSFile
 import com.google.devtools.ksp.symbol.KSName
+import com.google.devtools.ksp.symbol.KSPropertyDeclaration
 import com.google.devtools.ksp.symbol.KSType
 import com.google.devtools.ksp.symbol.KSTypeArgument
 import com.google.devtools.ksp.symbol.KSTypeParameter
@@ -223,6 +224,14 @@
     return resolve().toTypeName(args)
 }
 
+fun KSPropertyDeclaration.getQualifiedName(): String {
+    val qualifier =
+        qualifiedName?.getQualifier()
+            ?: throw ProcessingException("Unable to resolve the qualified name", this)
+    val simpleName = simpleName.asString()
+    return "$qualifier#$simpleName"
+}
+
 internal fun TypeName.ignoreNullable(): TypeName {
     return copy(nullable = false)
 }
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionComponentRegistryProcessor.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionComponentRegistryProcessor.kt
index 4871068..458d8c0 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionComponentRegistryProcessor.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionComponentRegistryProcessor.kt
@@ -107,7 +107,7 @@
                         qualifiedName = annotatedSerializable.jvmQualifiedName,
                         docString =
                             if (annotatedSerializable.isDescribedByKdoc) {
-                                annotatedSerializable.description
+                                annotatedSerializable.getDescription()
                             } else {
                                 ""
                             },
@@ -116,8 +116,7 @@
                 for (property in annotatedSerializable.getProperties()) {
                     add(
                         AppFunctionComponent(
-                            qualifiedName =
-                                "${annotatedSerializable.jvmQualifiedName}#${property.name}",
+                            qualifiedName = property.qualifiedName,
                             docString = property.description,
                         )
                     )
diff --git a/appfunctions/appfunctions-testing/build.gradle b/appfunctions/appfunctions-testing/build.gradle
index 2bd3a6f..89fec6c 100644
--- a/appfunctions/appfunctions-testing/build.gradle
+++ b/appfunctions/appfunctions-testing/build.gradle
@@ -36,6 +36,7 @@
     implementation(project(":appfunctions:appfunctions"))
     implementation(project(":appfunctions:appfunctions-service"))
     implementation("junit:junit:4.13.2")
+    implementation(libs.robolectric)
 
     // Internal dependencies
     implementation("androidx.annotation:annotation:1.9.0")
diff --git a/appfunctions/appfunctions-testing/src/main/java/androidx/appfunctions/testing/AppFunctionTestRule.kt b/appfunctions/appfunctions-testing/src/main/java/androidx/appfunctions/testing/AppFunctionTestRule.kt
index 864542d..8bfc485 100644
--- a/appfunctions/appfunctions-testing/src/main/java/androidx/appfunctions/testing/AppFunctionTestRule.kt
+++ b/appfunctions/appfunctions-testing/src/main/java/androidx/appfunctions/testing/AppFunctionTestRule.kt
@@ -26,6 +26,7 @@
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
+import org.robolectric.shadows.ShadowSystemProperties
 
 /**
  * A JUnit TestRule for setting up an environment to exercise AppFunction APIs in unit or
@@ -160,6 +161,10 @@
         object : Statement() {
             override fun evaluate() {
                 base?.evaluate()
+                // Robolectric platform doesn't set these properties, we have checks for certain
+                // AppSearch features that are only available if the sdk extensions for T are above
+                // 13.
+                ShadowSystemProperties.override(T_EXTENSION_PROPERTY_STRING, "13")
             }
         }
 
@@ -175,4 +180,8 @@
             translatorSelector = NullTranslatorSelector(),
         )
     }
+
+    private companion object {
+        private const val T_EXTENSION_PROPERTY_STRING = "build.version.extensions.t"
+    }
 }
diff --git a/appfunctions/appfunctions/src/main/java/androidx/appfunctions/AppFunctionManagerCompat.kt b/appfunctions/appfunctions/src/main/java/androidx/appfunctions/AppFunctionManagerCompat.kt
index e4c3dae..e540c5f 100644
--- a/appfunctions/appfunctions/src/main/java/androidx/appfunctions/AppFunctionManagerCompat.kt
+++ b/appfunctions/appfunctions/src/main/java/androidx/appfunctions/AppFunctionManagerCompat.kt
@@ -172,18 +172,23 @@
     }
 
     /**
-     * Observes for available app functions metadata based on the provided filters.
+     * Observes available app functions metadata based on the provided filters.
      *
      * Allows discovering app functions that match the given [searchSpec] criteria and continuously
-     * emits updates when relevant metadata changes. The calling app can only observe metadata for
-     * functions in packages that it is allowed to query via
-     * [android.content.pm.PackageManager.canPackageQuery]. If a package is not queryable by the
-     * calling app, its functions' metadata will not be visible.
+     * emits updates when relevant metadata changes.
      *
      * Updates to [AppFunctionPackageMetadata] can occur when the app defining the function is
-     * updated or when a function's enabled state changes.
+     * updated or when a function's enabled state changes, and if multiple updates happen within a
+     * short duration, only the latest update might be emitted.
      *
-     * If multiple updates happen within a short duration, only the latest update might be emitted.
+     * The calling app can observe metadata for:
+     * - Functions in its own package (no permission required).
+     * - Functions in other packages that it is allowed to query via
+     *   [android.content.pm.PackageManager.canPackageQuery] and when holding the
+     *   `android.permission.EXECUTE_APP_FUNCTIONS` permission.
+     *
+     * If a package is not queryable by the calling app, its functions' metadata will not be
+     * visible, even when holding the `android.permission.EXECUTE_APP_FUNCTIONS` permission.
      *
      * @param searchSpec an [AppFunctionSearchSpec] instance specifying the filters for searching
      *   the app function metadata.
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 702e668..9f491fc 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -561,7 +561,6 @@
     private fun configureWithAppPlugin(project: Project, androidXExtension: AndroidXExtension) {
         project.extensions.getByType<ApplicationExtension>().apply {
             configureAndroidBaseOptions(project, androidXExtension)
-            @Suppress("deprecation") // TODO(aurimas): migrate to new API
             defaultConfig.targetSdk = project.defaultAndroidConfig.targetSdk
             val debugSigningConfig = signingConfigs.getByName("debug")
             // Use a local debug keystore to avoid build server issues.
@@ -598,7 +597,6 @@
     private fun configureWithTestPlugin(project: Project, androidXExtension: AndroidXExtension) {
         project.extensions.getByType<TestExtension>().apply {
             configureAndroidBaseOptions(project, androidXExtension)
-            @Suppress("deprecation") // TODO(aurimas): migrate to new API
             defaultConfig.targetSdk = project.defaultAndroidConfig.targetSdk
             val debugSigningConfig = signingConfigs.getByName("debug")
             // Use a local debug keystore to avoid build server issues.
@@ -885,7 +883,6 @@
 
         libraryAndroidComponentsExtension.apply {
             finalizeDsl {
-                @Suppress("deprecation") // TODO(aurimas): migrate to new API
                 it.defaultConfig.aarMetadata.configure(it.compileSdk)
                 it.lint.targetSdk = project.defaultAndroidConfig.targetSdk
                 it.testOptions.targetSdk = project.defaultAndroidConfig.targetSdk
@@ -1054,19 +1051,16 @@
         // Suppress output of android:compileSdkVersion and related attributes (b/277836549).
         androidResources.additionalParameters += "--no-compile-sdk-metadata"
 
-        @Suppress("deprecation") // TODO(aurimas): migrate to new API
         compileSdk = project.defaultAndroidConfig.compileSdk
 
         buildToolsVersion = project.defaultAndroidConfig.buildToolsVersion
 
         defaultConfig.ndk.abiFilters.addAll(SUPPORTED_BUILD_ABIS)
-        @Suppress("DEPRECATION") // TODO(aurimas): migrate to new API
         defaultConfig.minSdk = defaultMinSdk
         defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
 
         testOptions.animationsDisabled = !project.isMacrobenchmark()
 
-        @Suppress("deprecation") // TODO(aurimas): migrate to new API
         project.afterEvaluate {
             check(
                 !androidXExtension.shouldPublish() || !compileOptions.isCoreLibraryDesugaringEnabled
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
index 4a4adfc..e65cbf3 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
@@ -748,6 +748,10 @@
                 configureDefaultIncrementalSyncTask()
                 configureKotlinJsTests()
                 configureNode()
+
+                // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same:
+                // https://youtrack.jetbrains.com/issue/KT-71032
+                configurePinnedKotlinLibraries(platform)
             }
         } else null
     }
@@ -836,6 +840,9 @@
 
 @OptIn(ExperimentalWasmDsl::class)
 private fun Project.configureBinaryen() {
+    if (ProjectLayoutType.isPlayground(project)) {
+        return
+    }
     plugins.withType<BinaryenPlugin>().configureEach {
         the<BinaryenEnvSpec>()
             .downloadBaseUrl
@@ -847,6 +854,25 @@
     }
 }
 
+private fun Project.configurePinnedKotlinLibraries(platform: PlatformIdentifier) {
+    multiplatformExtension?.let {
+        val kotlinLibSuffix =
+            when (platform) {
+                PlatformIdentifier.JS -> "js"
+                PlatformIdentifier.WASM_JS -> "wasm-js"
+                else -> throw IllegalStateException("Unsupported platform: $platform")
+            }
+        val kotlinVersion = project.getVersionByName("kotlin")
+        it.sourceSets.getByName("${platform.id}Main").dependencies {
+            implementation("org.jetbrains.kotlin:kotlin-stdlib-$kotlinLibSuffix:$kotlinVersion")
+        }
+        it.sourceSets.getByName("${platform.id}Test").dependencies {
+            implementation("org.jetbrains.kotlin:kotlin-stdlib-$kotlinLibSuffix:$kotlinVersion")
+            implementation("org.jetbrains.kotlin:kotlin-test-$kotlinLibSuffix:$kotlinVersion")
+        }
+    }
+}
+
 private fun Project.configureKotlinJsTests() {
     tasks.withType(KotlinJsTest::class.java).configureEach { task ->
         if (!ProjectLayoutType.isPlayground(this)) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/DevelocityTokenFetcher.kt b/buildSrc/private/src/main/kotlin/androidx/build/DevelocityTokenFetcher.kt
index 3087d26..ad61749 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/DevelocityTokenFetcher.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/DevelocityTokenFetcher.kt
@@ -29,7 +29,7 @@
  */
 internal fun Project.fetchDevelocityKeysIfNeeded() {
     // We are in CI, so we should not fetch these keys
-    if (System.getenv("BUILD_NUMBER") != null) return
+    if (System.getenv("IS_ANDROIDX_CI") != null) return
 
     // User does not have remote cache enabled, so we will not have access to GCP
     if (System.getenv("USE_ANDROIDX_REMOTE_BUILD_CACHE") !in setOf("gcp", "true")) return
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
index b9742ea..ff4176e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
@@ -170,7 +170,6 @@
             val addStubAar = isKmpAnchor && pomPlatform == PlatformIdentifier.ANDROID.id
             val buildDir = project.layout.buildDirectory
             if (addStubAar) {
-                @Suppress("DEPRECATION") // TODO(aurimas): migrate to new API
                 val minSdk =
                     project.extensions.findByType<LibraryExtension>()?.defaultConfig?.minSdk
                         ?: extensions
@@ -242,14 +241,18 @@
         }
     }
 
+    val buildIdProvider = project.providers.getBuildId()
     // Workaround for https://github.com/gradle/gradle/issues/31218
     project.tasks.withType(GenerateModuleMetadata::class.java).configureEach { task ->
         task.doLast {
-            val metadata = task.outputFile.asFile.get()
-            val text = metadata.readText()
-            metadata.writeText(
-                text.replace("\"buildId\": .*".toRegex(), "\"buildId:\": \"${getBuildId()}\"")
-            )
+            if (buildIdProvider.isPresent) {
+                val buildId = buildIdProvider.get()
+                val metadata = task.outputFile.asFile.get()
+                val text = metadata.readText()
+                metadata.writeText(
+                    text.replace("\"buildId\": .*".toRegex(), "\"buildId:\": \"${buildId}\"")
+                )
+            }
         }
     }
 }
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 bb337f0..9acdb5a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -104,7 +104,6 @@
             when (plugin) {
                 is LibraryPlugin -> {
                     val libraryExtension = project.extensions.getByType<LibraryExtension>()
-                    @Suppress("deprecation") // TODO(aurimas): migrate to new API
                     libraryExtension.compileSdk =
                         project.defaultAndroidConfig.latestStableCompileSdk
                     libraryExtension.buildToolsVersion =
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 98d624e..6a2850f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -162,7 +162,7 @@
             task.additionalTags.set(androidXExtension.additionalDeviceTestTags)
             task.outputXml.set(getFileInTestConfigDirectory(xmlName))
             jsonName?.let { task.outputJson.set(getFileInTestConfigDirectory(it)) }
-            task.presubmit.set(isPresubmitBuild())
+            task.presubmit.set(project.providers.isPresubmitBuild())
             task.instrumentationArgs.putAll(instrumentationRunnerArgs)
             task.minSdk.set(minSdk)
             task.hasBenchmarkPlugin.set(hasBenchmarkPlugin())
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/BuildServerConfiguration.kt b/buildSrc/public/src/main/kotlin/androidx/build/BuildServerConfiguration.kt
index 4b8281b..aa2a200 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/BuildServerConfiguration.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/BuildServerConfiguration.kt
@@ -22,6 +22,7 @@
 import org.gradle.api.file.Directory
 import org.gradle.api.file.RegularFile
 import org.gradle.api.provider.Provider
+import org.gradle.api.provider.ProviderFactory
 
 /**
  * @return build id string for current build
@@ -29,13 +30,7 @@
  * The build server does not pass the build id so we infer it from the last folder of the
  * distribution directory name.
  */
-fun getBuildId(): String {
-    return if (System.getenv("BUILD_NUMBER") != null) {
-        System.getenv("BUILD_NUMBER")
-    } else {
-        "0"
-    }
-}
+fun ProviderFactory.getBuildId(): Provider<String> = environmentVariable("BUILD_NUMBER")
 
 /**
  * Gets set to true when the build id is prefixed with P.
@@ -43,12 +38,8 @@
  * In AffectedModuleDetector, we return a different ProjectSubset in presubmit vs. postsubmit, to
  * get the desired test behaviors.
  */
-fun isPresubmitBuild(): Boolean {
-    return if (System.getenv("BUILD_NUMBER") != null) {
-        System.getenv("BUILD_NUMBER").startsWith("P")
-    } else {
-        false
-    }
+fun ProviderFactory.isPresubmitBuild(): Provider<Boolean> {
+    return environmentVariable("BUILD_NUMBER").map { it.startsWith("P") }.orElse(false)
 }
 
 /**
diff --git a/busytown/impl/build.sh b/busytown/impl/build.sh
index dfdb0b9..674243b 100755
--- a/busytown/impl/build.sh
+++ b/busytown/impl/build.sh
@@ -32,6 +32,7 @@
   IS_POSTSUBMIT=false
 fi
 export IS_POSTSUBMIT
+export IS_ANDROIDX_CI=true
 
 # parse arguments
 if [ "$1" == "--diagnose" ]; then
diff --git a/camera/camera-camera2-pipe-integration/build.gradle b/camera/camera-camera2-pipe-integration/build.gradle
index 53b2910..fe95b3e 100644
--- a/camera/camera-camera2-pipe-integration/build.gradle
+++ b/camera/camera-camera2-pipe-integration/build.gradle
@@ -75,7 +75,7 @@
     testImplementation("org.robolectric:shadowapi:4.14")
     testImplementation("org.robolectric:shadows-framework:4.14")
     testImplementation(libs.guavaAndroid)
-    testImplementation(libs.kotlinTestForWasmTests)
+    testImplementation(libs.kotlinTest)
     testImplementation(libs.testMonitor)
 
     androidTestImplementation(libs.testExtJunit)
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZslControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZslControl.kt
index 6da1081..bfb6e9e 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZslControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZslControl.kt
@@ -21,9 +21,7 @@
 import android.hardware.camera2.CameraDevice
 import android.hardware.camera2.params.InputConfiguration
 import android.hardware.camera2.params.StreamConfigurationMap
-import android.os.Build
 import android.util.Size
-import androidx.annotation.RequiresApi
 import androidx.annotation.VisibleForTesting
 import androidx.camera.camera2.pipe.CameraMetadata.Companion.supportsPrivateReprocessing
 import androidx.camera.camera2.pipe.core.Log
@@ -111,7 +109,6 @@
     public fun dequeueImageFromBuffer(): ImageProxy?
 }
 
-@RequiresApi(Build.VERSION_CODES.M)
 @CameraScope
 public class ZslControlImpl @Inject constructor(private val cameraProperties: CameraProperties) :
     ZslControl {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FinalizeSessionOnCloseQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FinalizeSessionOnCloseQuirk.kt
index 45c4ac5..adab96f3 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FinalizeSessionOnCloseQuirk.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/FinalizeSessionOnCloseQuirk.kt
@@ -52,14 +52,6 @@
                 // before we use this workaround to finalize the capture session, and thereby
                 // releasing the Surfaces.
                 FinalizeSessionOnCloseBehavior.IMMEDIATE
-            } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
-                // When CloseCaptureSessionOnVideoQuirk is enabled, we close the capture session
-                // in anticipation that the onClosed() callback would finalize the session. However,
-                // on API levels < M, it could be possible that onClosed() isn't invoked if a new
-                // capture session (or CameraGraph) is created too soon (read b/144817309 or
-                // CaptureSessionOnClosedNotCalledQuirk for more context). Therefore, we're enabling
-                // this quirk (on a timeout) for API levels < M, too.
-                FinalizeSessionOnCloseBehavior.TIMEOUT
             } else {
                 FinalizeSessionOnCloseBehavior.OFF
             }
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 68e5959..128593c 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
@@ -18,7 +18,6 @@
 
 import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.params.StreamConfigurationMap
-import android.os.Build
 import androidx.annotation.Nullable
 import androidx.annotation.VisibleForTesting
 import androidx.camera.camera2.pipe.CameraId
@@ -32,7 +31,6 @@
 import androidx.camera.camera2.pipe.integration.adapter.EncoderProfilesProviderAdapter
 import androidx.camera.camera2.pipe.integration.adapter.ZslControl
 import androidx.camera.camera2.pipe.integration.adapter.ZslControlImpl
-import androidx.camera.camera2.pipe.integration.adapter.ZslControlNoOpImpl
 import androidx.camera.camera2.pipe.integration.compat.Camera2CameraControlCompat
 import androidx.camera.camera2.pipe.integration.compat.CameraCompatModule
 import androidx.camera.camera2.pipe.integration.compat.EvCompCompat
@@ -163,11 +161,7 @@
         @CameraScope
         @Provides
         public fun provideZslControl(cameraProperties: CameraProperties): ZslControl {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                return ZslControlImpl(cameraProperties)
-            } else {
-                return ZslControlNoOpImpl()
-            }
+            return ZslControlImpl(cameraProperties)
         }
 
         @CameraScope
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
index c324c9d..efa5d25 100644
--- 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
@@ -162,24 +162,8 @@
             captureSessionId: CameraInterop.CameraCaptureSessionId,
             surface: Surface,
         ) {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                Api23CompatImpl.onSurfacePrepared(placeholderSession, surface, callbacks)
-            } else {
-                Log.error { "onSurfacePrepared called for unsupported OS version." }
-            }
-        }
-
-        @RequiresApi(Build.VERSION_CODES.M)
-        private object Api23CompatImpl {
-            @JvmStatic
-            fun onSurfacePrepared(
-                session: CameraCaptureSession,
-                surface: Surface,
-                callbacks: AtomicRef<List<CameraCaptureSession.StateCallback>>,
-            ) {
-                for (callback in callbacks.value) {
-                    callback.onSurfacePrepared(session, surface)
-                }
+            for (callback in callbacks.value) {
+                callback.onSurfacePrepared(placeholderSession, surface)
             }
         }
 
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 6d52745..2ca912c 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
@@ -16,7 +16,6 @@
 
 package androidx.camera.camera2.pipe.compat
 
-import android.content.Context
 import android.graphics.ColorSpace
 import android.hardware.camera2.CameraAccessException
 import android.hardware.camera2.CameraCaptureSession
@@ -45,63 +44,6 @@
 import androidx.camera.camera2.pipe.CameraMetadata.Companion.availableVideoStabilizationModes
 import java.util.concurrent.Executor
 
-@RequiresApi(23)
-internal object Api23Compat {
-    @JvmStatic
-    @Throws(CameraAccessException::class)
-    @Suppress("deprecation")
-    fun createReprocessableCaptureSession(
-        cameraDevice: CameraDevice,
-        inputConfig: InputConfiguration,
-        outputs: List<Surface>,
-        callback: CameraCaptureSession.StateCallback,
-        handler: Handler?,
-    ) {
-        cameraDevice.createReprocessableCaptureSession(inputConfig, outputs, callback, handler)
-    }
-
-    @JvmStatic
-    @Throws(CameraAccessException::class)
-    @Suppress("deprecation")
-    fun createConstrainedHighSpeedCaptureSession(
-        cameraDevice: CameraDevice,
-        outputs: List<Surface>,
-        stateCallback: CameraCaptureSession.StateCallback,
-        handler: Handler?,
-    ) {
-        cameraDevice.createConstrainedHighSpeedCaptureSession(outputs, stateCallback, handler)
-    }
-
-    @JvmStatic
-    @Throws(CameraAccessException::class)
-    fun createReprocessCaptureRequest(
-        cameraDevice: CameraDevice,
-        inputResult: TotalCaptureResult,
-    ): CaptureRequest.Builder {
-        return cameraDevice.createReprocessCaptureRequest(inputResult)
-    }
-
-    @JvmStatic
-    fun isReprocessable(cameraCaptureSession: CameraCaptureSession): Boolean {
-        return cameraCaptureSession.isReprocessable
-    }
-
-    @JvmStatic
-    fun getInputSurface(cameraCaptureSession: CameraCaptureSession): Surface? {
-        return cameraCaptureSession.inputSurface
-    }
-
-    @JvmStatic
-    fun newInputConfiguration(width: Int, height: Int, format: Int): InputConfiguration {
-        return InputConfiguration(width, height, format)
-    }
-
-    @JvmStatic
-    fun checkSelfPermission(context: Context, permission: String): Int {
-        return context.checkSelfPermission(permission)
-    }
-}
-
 @RequiresApi(24)
 internal object Api24Compat {
     @JvmStatic
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 8f1069a..ec5f04b 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
@@ -155,7 +155,7 @@
             // surface per request.
             check(hasSurface)
 
-            if (request.inputRequest != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            if (request.inputRequest != null) {
                 checkNotNull(imageWriter) {
                     "Failed to create ImageWriter for capture session: $session"
                 }
@@ -387,7 +387,7 @@
      * created, assuming it's a reprocessing session.
      */
     private val imageWriter =
-        if (streamGraph.inputs.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+        if (streamGraph.inputs.isNotEmpty()) {
             val inputStream = streamGraph.inputs.first()
             val sessionInputSurface = session.inputSurface
             checkNotNull(sessionInputSurface) {
@@ -558,17 +558,13 @@
     ): CaptureRequest.Builder? {
         val requestBuilder =
             if (request.inputRequest != null) {
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                    val totalCaptureResult =
-                        request.inputRequest.frameInfo.unwrapAs(TotalCaptureResult::class)
-                    checkNotNull(totalCaptureResult) {
-                        "Failed to unwrap FrameInfo ${request.inputRequest.frameInfo} as " +
-                            "TotalCaptureResult"
-                    }
-                    session.device.createReprocessCaptureRequest(totalCaptureResult)
-                } else {
-                    null
+                val totalCaptureResult =
+                    request.inputRequest.frameInfo.unwrapAs(TotalCaptureResult::class)
+                checkNotNull(totalCaptureResult) {
+                    "Failed to unwrap FrameInfo ${request.inputRequest.frameInfo} as " +
+                        "TotalCaptureResult"
                 }
+                session.device.createReprocessCaptureRequest(totalCaptureResult)
             } else {
                 session.device.createCaptureRequest(requestTemplate)
             }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt
index 11f7b35..ee8bbaae 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2Quirks.kt
@@ -22,6 +22,7 @@
 import androidx.camera.camera2.pipe.CameraGraph.RepeatingRequestRequirementsBeforeCapture.CompletionBehavior.EXACT
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.CameraMetadata.Companion.isHardwareLevelLegacy
+import androidx.camera.camera2.pipe.compat.Camera2Quirks.Companion.SHOULD_WAIT_FOR_REPEATING_DEVICE_MAP
 import javax.inject.Inject
 import javax.inject.Singleton
 import kotlin.math.max
@@ -95,7 +96,7 @@
      */
     internal fun shouldCloseCameraBeforeCreatingCaptureSession(cameraId: CameraId): Boolean {
         val isLegacyDevice =
-            Build.VERSION.SDK_INT in (Build.VERSION_CODES.M..Build.VERSION_CODES.S_V2) &&
+            Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2 &&
                 metadataProvider.awaitCameraMetadata(cameraId).isHardwareLevelLegacy
         val isQuirkyDevice =
             "motorola".equals(Build.BRAND, ignoreCase = true) &&
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 12081ac..6680082 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
@@ -53,7 +53,6 @@
     fun createCaptureRequest(template: RequestTemplate): CaptureRequest.Builder?
 
     /** @see CameraDevice.createReprocessCaptureRequest */
-    @RequiresApi(23)
     fun createReprocessCaptureRequest(inputResult: TotalCaptureResult): CaptureRequest.Builder?
 
     /** @see CameraDevice.createCaptureSession */
@@ -63,7 +62,6 @@
     ): Boolean
 
     /** @see CameraDevice.createReprocessableCaptureSession */
-    @RequiresApi(23)
     fun createReprocessableCaptureSession(
         input: InputConfiguration,
         outputs: List<Surface>,
@@ -71,7 +69,6 @@
     ): Boolean
 
     /** @see CameraDevice.createConstrainedHighSpeedCaptureSession */
-    @RequiresApi(23)
     fun createConstrainedHighSpeedCaptureSession(
         outputs: List<Surface>,
         stateCallback: CameraCaptureSessionWrapper.StateCallback,
@@ -216,7 +213,7 @@
         return result != null
     }
 
-    @RequiresApi(23)
+    @Suppress("deprecation")
     override fun createReprocessableCaptureSession(
         input: InputConfiguration,
         outputs: List<Surface>,
@@ -229,8 +226,7 @@
             instrumentAndCatch("createReprocessableCaptureSession") {
                 // This function was deprecated in Android Q, but is required for some
                 // configurations when running on older versions of the OS.
-                Api23Compat.createReprocessableCaptureSession(
-                    cameraDevice,
+                cameraDevice.createReprocessableCaptureSession(
                     input,
                     outputs,
                     AndroidCaptureSessionStateCallback(
@@ -256,7 +252,7 @@
         return result != null
     }
 
-    @RequiresApi(23)
+    @Suppress("deprecation")
     override fun createConstrainedHighSpeedCaptureSession(
         outputs: List<Surface>,
         stateCallback: CameraCaptureSessionWrapper.StateCallback,
@@ -269,8 +265,7 @@
                 // This function was deprecated in Android Q, but is required for some
                 // configurations
                 // when running on older versions of the OS.
-                Api23Compat.createConstrainedHighSpeedCaptureSession(
-                    cameraDevice,
+                cameraDevice.createConstrainedHighSpeedCaptureSession(
                     outputs,
                     AndroidCaptureSessionStateCallback(
                         this,
@@ -349,11 +344,7 @@
                 // configurations when running on older versions of the OS.
                 Api24Compat.createReprocessableCaptureSessionByConfigurations(
                     cameraDevice,
-                    Api23Compat.newInputConfiguration(
-                        inputConfig.width,
-                        inputConfig.height,
-                        inputConfig.format,
-                    ),
+                    InputConfiguration(inputConfig.width, inputConfig.height, inputConfig.format),
                     outputs.map { it.unwrapAs(OutputConfiguration::class) },
                     AndroidCaptureSessionStateCallback(
                         this,
@@ -412,7 +403,7 @@
                     } else {
                         Api28Compat.setInputConfiguration(
                             sessionConfig,
-                            Api23Compat.newInputConfiguration(
+                            InputConfiguration(
                                 config.inputConfiguration.single().width,
                                 config.inputConfiguration.single().height,
                                 config.inputConfiguration.single().format,
@@ -479,12 +470,11 @@
             cameraDevice.createCaptureRequest(template.value)
         }
 
-    @RequiresApi(23)
     override fun createReprocessCaptureRequest(
         inputResult: TotalCaptureResult
     ): CaptureRequest.Builder? =
         instrumentAndCatch("createReprocessCaptureRequest") {
-            Api23Compat.createReprocessCaptureRequest(cameraDevice, inputResult)
+            cameraDevice.createReprocessCaptureRequest(inputResult)
         }
 
     @RequiresApi(30)
@@ -577,7 +567,6 @@
             }
         }
 
-    @RequiresApi(23)
     override fun createReprocessableCaptureSession(
         input: InputConfiguration,
         outputs: List<Surface>,
@@ -593,7 +582,6 @@
             }
         }
 
-    @RequiresApi(23)
     override fun createConstrainedHighSpeedCaptureSession(
         outputs: List<Surface>,
         stateCallback: CameraCaptureSessionWrapper.StateCallback,
@@ -687,7 +675,6 @@
             }
         }
 
-    @RequiresApi(23)
     override fun createReprocessCaptureRequest(inputResult: TotalCaptureResult) =
         synchronized(lock) {
             if (disconnected) {
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 b93ff84..9b1b156 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
@@ -53,7 +53,6 @@
     @Camera2ControllerScope
     @Provides
     fun provideSessionFactory(
-        androidLProvider: Provider<AndroidLSessionFactory>,
         androidMProvider: Provider<AndroidMSessionFactory>,
         androidMHighSpeedProvider: Provider<AndroidMHighSpeedSessionFactory>,
         androidNProvider: Provider<AndroidNSessionFactory>,
@@ -73,9 +72,6 @@
         }
 
         if (graphConfig.sessionMode == CameraGraph.OperatingMode.HIGH_SPEED) {
-            check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                "Cannot use HighSpeed sessions below Android M"
-            }
             return androidMHighSpeedProvider.get()
         }
 
@@ -83,34 +79,11 @@
             return androidNProvider.get()
         }
 
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            return androidMProvider.get()
-        }
-
-        check(graphConfig.input == null) { "Reprocessing is not supported on Android L" }
-
-        return androidLProvider.get()
+        check(graphConfig.input == null) { "Reprocessing is not supported on Android M" }
+        return androidMProvider.get()
     }
 }
 
-internal class AndroidLSessionFactory @Inject constructor(private val threads: Threads) :
-    CaptureSessionFactory {
-    override fun create(
-        cameraDevice: CameraDeviceWrapper,
-        surfaces: Map<StreamId, Surface>,
-        captureSessionState: CaptureSessionState,
-    ): Map<StreamId, OutputConfigurationWrapper> {
-        if (!cameraDevice.createCaptureSession(surfaces.map { it.value }, captureSessionState)) {
-            Log.warn {
-                "Failed to create capture session from $cameraDevice for $captureSessionState!"
-            }
-            captureSessionState.onSessionFinalized()
-        }
-        return emptyMap()
-    }
-}
-
-@RequiresApi(23)
 internal class AndroidMSessionFactory
 @Inject
 constructor(private val threads: Threads, private val graphConfig: CameraGraph.Config) :
@@ -153,7 +126,6 @@
     }
 }
 
-@RequiresApi(23)
 internal class AndroidMHighSpeedSessionFactory @Inject constructor(private val threads: Threads) :
     CaptureSessionFactory {
     override fun create(
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 d7e2840..102f717 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
@@ -222,9 +222,7 @@
         // return a CameraConstrainedHighSpeedCaptureSession depending on the configuration. If
         // 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
-        ) {
+        return if (session is CameraConstrainedHighSpeedCaptureSession) {
             AndroidCameraConstrainedHighSpeedCaptureSession(
                 device,
                 session,
@@ -299,23 +297,10 @@
         CameraInterop.nextCameraCaptureSessionId()
 
     override val isReprocessable: Boolean
-        get() {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                return Api23Compat.isReprocessable(cameraCaptureSession)
-            }
-            // Reprocessing is not supported  prior to Android M
-            return false
-        }
+        get() = cameraCaptureSession.isReprocessable
 
     override val inputSurface: Surface?
-        get() {
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                return Api23Compat.getInputSurface(cameraCaptureSession)
-            }
-            // Reprocessing is not supported prior to Android M, and a CaptureSession that does not
-            // support reprocessing will have a null input surface on M and beyond.
-            return null
-        }
+        get() = cameraCaptureSession.inputSurface
 
     @RequiresApi(26)
     override fun finalizeOutputConfigurations(
@@ -358,7 +343,6 @@
  * An implementation of [CameraConstrainedHighSpeedCaptureSessionWrapper] forwards calls to a real
  * [CameraConstrainedHighSpeedCaptureSession].
  */
-@RequiresApi(23)
 internal class AndroidCameraConstrainedHighSpeedCaptureSession
 internal constructor(
     device: CameraDeviceWrapper,
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 bb2e486..ae25eb0 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
@@ -20,8 +20,6 @@
 import android.content.Context
 import android.content.pm.PackageManager.PERMISSION_GRANTED
 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
@@ -38,16 +36,8 @@
 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() = checkCameraPermission()
 
-    @RequiresApi(23)
     private fun checkCameraPermission(): Boolean {
         if (Build.FINGERPRINT == "robolectric") {
             // If we're running under Robolectric, assume we have camera permission since
@@ -63,7 +53,7 @@
         if (!_hasCameraPermission) {
             Debug.traceStart { "CXCP#checkCameraPermission" }
             if (
-                Api23Compat.checkSelfPermission(cameraPipeContext, Manifest.permission.CAMERA) ==
+                cameraPipeContext.checkSelfPermission(Manifest.permission.CAMERA) ==
                     PERMISSION_GRANTED
             ) {
                 _hasCameraPermission = true
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 da77b7a..2d96e71 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
@@ -25,6 +25,7 @@
 import androidx.camera.camera2.pipe.AwbMode
 import androidx.camera.camera2.pipe.CameraController
 import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraGraph.Session
 import androidx.camera.camera2.pipe.CameraGraphId
 import androidx.camera.camera2.pipe.CameraMetadata
 import androidx.camera.camera2.pipe.FrameMetadata
@@ -46,7 +47,6 @@
 import androidx.camera.camera2.pipe.internal.FrameCaptureQueue
 import androidx.camera.camera2.pipe.internal.FrameDistributor
 import javax.inject.Inject
-import kotlin.use
 import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
@@ -107,10 +107,7 @@
             }
         }
 
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
-            require(graphConfig.input == null) { "Reprocessing not supported under Android M" }
-        }
-        if (graphConfig.input != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+        if (graphConfig.input != null) {
             require(graphConfig.input.isNotEmpty()) {
                 "At least one InputConfiguration is required for reprocessing"
             }
@@ -154,7 +151,7 @@
         Debug.traceStop()
     }
 
-    override suspend fun acquireSession(): CameraGraph.Session {
+    override suspend fun acquireSession(): Session {
         // Step 1: Acquire a lock on the session mutex, which returns a releasable token. This may
         //         or may not suspend.
         val token = sessionLock.acquireToken()
@@ -164,14 +161,12 @@
         return createSessionFromToken(token)
     }
 
-    override fun acquireSessionOrNull(): CameraGraph.Session? {
+    override fun acquireSessionOrNull(): Session? {
         val token = sessionLock.tryAcquireToken() ?: return null
         return createSessionFromToken(token)
     }
 
-    override suspend fun <T> useSession(
-        action: suspend CoroutineScope.(CameraGraph.Session) -> T
-    ): T =
+    override suspend fun <T> useSession(action: suspend CoroutineScope.(Session) -> T): T =
         acquireSession().use {
             // Wrap the block in a coroutineScope to ensure all operations are completed before
             // releasing the lock.
@@ -180,7 +175,7 @@
 
     override fun <T> useSessionIn(
         scope: CoroutineScope,
-        action: suspend CoroutineScope.(CameraGraph.Session) -> T,
+        action: suspend CoroutineScope.(Session) -> T,
     ): Deferred<T> {
         return sessionLock.withTokenIn(scope) { token ->
             // Create and use the session
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 6e7af11..cc10bdd 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
@@ -28,9 +28,7 @@
 import android.hardware.camera2.CaptureRequest.CONTROL_AF_TRIGGER_START
 import android.hardware.camera2.CaptureResult
 import android.hardware.camera2.params.MeteringRectangle
-import android.os.Build
 import androidx.annotation.GuardedBy
-import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.AeMode
 import androidx.camera.camera2.pipe.AfMode
 import androidx.camera.camera2.pipe.AwbMode
@@ -587,40 +585,7 @@
         if (graphProcessor.repeatingRequest == null) {
             return deferredResult3ASubmitFailed
         }
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            return unlock3APostCaptureAndroidMAndAbove(cancelAf)
-        }
-        return unlock3APostCaptureAndroidLAndBelow(cancelAf)
-    }
-
-    /**
-     * 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 fun unlock3APostCaptureAndroidLAndBelow(cancelAf: Boolean = true): Deferred<Result3A> {
-        debug { "unlock3AForCapture - sending a request to cancel af and turn on ae." }
-        val cancelParams =
-            if (cancelAf) {
-                unlock3APostCaptureLockAeAndCancelAfParams
-            } else {
-                unlock3APostCaptureLockAeParams
-            }
-        if (!graphProcessor.trigger(cancelParams)) return deferredResult3ASubmitFailed
-
-        // Listener to monitor when we receive the capture result corresponding to the request
-        // below.
-        val listener = Result3AStateListenerImpl(emptyMap())
-        graphListener3A.addListener(listener)
-
-        debug { "unlock3AForCapture - sending a request to turn off ae." }
-        if (!graphProcessor.trigger(unlock3APostCaptureUnlockAeParams)) {
-            graphListener3A.removeListener(listener)
-            return deferredResult3ASubmitFailed
-        }
-
-        return listener.result
+        return unlock3APostCaptureAndroidMAndAbove(cancelAf)
     }
 
     /**
@@ -628,7 +593,6 @@
      * = 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 fun unlock3APostCaptureAndroidMAndAbove(cancelAf: Boolean = true): Deferred<Result3A> {
         debug { "unlock3APostCapture - sending a request to reset af and ae precapture metering." }
         val cancelParams = if (cancelAf) aePrecaptureAndAfCancelParams else aePrecaptureCancelParams
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageWriter.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageWriter.kt
index b69a9d9..1c80885 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageWriter.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageWriter.kt
@@ -21,7 +21,6 @@
 import android.os.Build
 import android.os.Handler
 import android.view.Surface
-import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.InputStreamId
 import androidx.camera.camera2.pipe.StreamFormat
 import androidx.camera.camera2.pipe.compat.Api29Compat
@@ -31,7 +30,6 @@
 import kotlinx.atomicfu.atomic
 
 /** Implements an [ImageWriterWrapper] using an [ImageWriter]. */
-@RequiresApi(23)
 public class AndroidImageWriter
 private constructor(
     private val imageWriter: ImageWriter,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/ImageWriterWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/ImageWriterWrapper.kt
index faae846..113aef5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/ImageWriterWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/ImageWriterWrapper.kt
@@ -17,12 +17,10 @@
 package androidx.camera.camera2.pipe.media
 
 import android.media.ImageWriter
-import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.InputStreamId
 import androidx.camera.camera2.pipe.UnsafeWrapper
 
 /** Simplified wrapper for [ImageWriter]-like classes. */
-@RequiresApi(23)
 public interface ImageWriterWrapper : UnsafeWrapper, AutoCloseable {
 
     /**
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 a6ea031..bf05a42 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
@@ -20,7 +20,6 @@
 
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.CaptureResult
-import android.os.Build
 import androidx.camera.camera2.pipe.FrameMetadata
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.RequestNumber
@@ -395,20 +394,12 @@
 
     @Test
     fun testUnlock3APostCapture() {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            testUnlock3APostCaptureAndroidMAndAbove()
-        } else {
-            testUnlock3APostCaptureAndroidLAndBelow()
-        }
+        testUnlock3APostCaptureAndroidMAndAbove()
     }
 
     @Test
     fun testUnlock3APostCapture_whenAfNotTriggered() {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            testUnlock3APostCaptureAndroidMAndAbove(false)
-        } else {
-            testUnlock3APostCaptureAndroidLAndBelow(false)
-        }
+        testUnlock3APostCaptureAndroidMAndAbove(false)
     }
 
     private fun testUnlock3APostCaptureAndroidMAndAbove(cancelAf: Boolean = true) = runTest {
@@ -488,45 +479,6 @@
             )
     }
 
-    private fun testUnlock3APostCaptureAndroidLAndBelow(cancelAf: Boolean = true) = runTest {
-        val result = controller3A.unlock3APostCapture(cancelAf)
-        assertThat(result.isCompleted).isFalse()
-
-        val cameraResponse = async {
-            listener3A.onRequestSequenceCreated(
-                FakeRequestMetadata(requestNumber = RequestNumber(1))
-            )
-            listener3A.onPartialCaptureResult(
-                FakeRequestMetadata(requestNumber = RequestNumber(1)),
-                FrameNumber(101L),
-                FakeFrameMetadata(frameNumber = FrameNumber(101L), resultMetadata = mapOf()),
-            )
-        }
-
-        cameraResponse.await()
-        val result3A = result.await()
-        assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
-        assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
-
-        // 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 event1 = captureSequenceProcessor.nextEvent()
-        if (cancelAf) {
-            assertThat(event1.requiredParameters)
-                .containsEntry(
-                    CaptureRequest.CONTROL_AF_TRIGGER,
-                    CaptureRequest.CONTROL_AF_TRIGGER_CANCEL,
-                )
-        }
-
-        assertThat(event1.requiredParameters).containsEntry(CaptureRequest.CONTROL_AE_LOCK, true)
-
-        // Then another request to unlock ae.
-        val captureSequence2 = captureSequenceProcessor.nextEvent()
-        assertThat(captureSequence2.requiredParameters)
-            .containsEntry(CaptureRequest.CONTROL_AE_LOCK, false)
-    }
-
     private fun assertCorrectCaptureSequenceInLock3AForCapture(isAfTriggered: Boolean = true) {
         val event1 = captureSequenceProcessor.nextEvent()
         assertThat(event1.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).apply {
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 a0d5447..a88d926 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
@@ -20,13 +20,11 @@
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.TotalCaptureResult
 import android.hardware.camera2.params.InputConfiguration
-import android.os.Build
 import android.view.Surface
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.AudioRestrictionMode
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.RequestTemplate
-import androidx.camera.camera2.pipe.compat.Api23Compat
 import androidx.camera.camera2.pipe.compat.CameraCaptureSessionWrapper
 import androidx.camera.camera2.pipe.compat.CameraDeviceWrapper
 import androidx.camera.camera2.pipe.compat.CameraExtensionSessionWrapper
@@ -53,12 +51,7 @@
     override fun createReprocessCaptureRequest(
         inputResult: TotalCaptureResult
     ): CaptureRequest.Builder {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            return Api23Compat.createReprocessCaptureRequest(fakeCamera.cameraDevice, inputResult)
-        }
-        throw UnsupportedOperationException(
-            "createReprocessCaptureRequest is not supported below API 23"
-        )
+        return fakeCamera.cameraDevice.createReprocessCaptureRequest(inputResult)
     }
 
     override fun createCaptureSession(
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
index 2b5352e..680f627 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
@@ -18,7 +18,6 @@
 
 import static androidx.camera.core.CameraUnavailableException.CAMERA_ERROR;
 import static androidx.camera.core.impl.CameraValidator.CameraIdListIncorrectException;
-import static androidx.camera.core.impl.CameraValidator.validateCameras;
 
 import android.app.Application;
 import android.content.ComponentName;
@@ -47,6 +46,7 @@
 import androidx.camera.core.impl.CameraProviderExecutionState;
 import androidx.camera.core.impl.CameraRepository;
 import androidx.camera.core.impl.CameraThreadConfig;
+import androidx.camera.core.impl.CameraValidator;
 import androidx.camera.core.impl.MetadataHolderService;
 import androidx.camera.core.impl.QuirkSettings;
 import androidx.camera.core.impl.QuirkSettingsHolder;
@@ -401,6 +401,8 @@
 
                 CameraSelector availableCamerasLimiter =
                         mCameraXConfig.getAvailableCamerasLimiter(null);
+                CameraValidator cameraValidator =
+                        CameraValidator.create(appContext, availableCamerasLimiter);
                 long cameraOpenRetryMaxTimeoutInMillis =
                         mCameraXConfig.getCameraOpenRetryMaxTimeoutInMillisWhileResuming();
 
@@ -457,13 +459,13 @@
                             mCameraUseCaseAdapterProvider);
                 }
 
-                mCameraPresenceProvider.startup(mCameraFactory, mCameraRepository);
+                mCameraPresenceProvider.startup(cameraValidator, mCameraFactory, mCameraRepository);
                 mCameraPresenceProvider.addDependentInternalListener(mSurfaceManager);
                 mCameraPresenceProvider.addDependentInternalListener(
                         mCameraFactory.getCameraCoordinator());
 
                 // Please ensure only validate the camera at the last of the initialization.
-                validateCameras(appContext, mCameraRepository, availableCamerasLimiter);
+                cameraValidator.validateOnFirstInit(mCameraRepository);
 
                 // Set completer to null if the init was successful.
                 if (attemptCount > 1) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraPresenseProvider.kt b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraPresenceProvider.kt
similarity index 88%
rename from camera/camera-core/src/main/java/androidx/camera/core/impl/CameraPresenseProvider.kt
rename to camera/camera-core/src/main/java/androidx/camera/core/impl/CameraPresenceProvider.kt
index 8f2e783..781ad1c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraPresenseProvider.kt
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraPresenceProvider.kt
@@ -45,6 +45,7 @@
     private var cameraFactory: CameraFactory? = null
     private var cameraRepository: CameraRepository? = null
     private var sourcePresenceObservable: Observable<List<CameraIdentifier>>? = null
+    private var cameraValidator: CameraValidator? = null
     private val sourceObserver: SourceObservableObserver = SourceObservableObserver()
 
     @Volatile private var currentFilteredIds: List<CameraIdentifier> = emptyList()
@@ -64,15 +65,21 @@
     /**
      * Starts monitoring camera presence.
      *
+     * @param cameraValidator The validator to verify whether the change of the camera is valid.
      * @param cameraFactory The factory that provides the presence observable.
      * @param cameraRepository The repository to get camera instances from.
      */
-    public fun startup(cameraFactory: CameraFactory, cameraRepository: CameraRepository) {
+    public fun startup(
+        cameraValidator: CameraValidator,
+        cameraFactory: CameraFactory,
+        cameraRepository: CameraRepository,
+    ) {
         if (!isMonitoring.compareAndSet(false, true)) {
             return
         }
         Logger.i(TAG, "Starting CameraPresenceProvider monitoring.")
 
+        this.cameraValidator = cameraValidator
         this.currentFilteredIds =
             cameraFactory.availableCameraIds.map { CameraIdentifier.create(it) }
         this.cameraFactory = cameraFactory
@@ -96,6 +103,7 @@
         sourcePresenceObservable?.removeObserver(sourceObserver)
         clearAllCameraStateObservers()
 
+        cameraValidator = null
         dependentInternalListeners.clear()
         publicApiListeners.clear()
         currentFilteredIds = emptyList()
@@ -106,20 +114,50 @@
     private inner class SourceObservableObserver : Observable.Observer<List<CameraIdentifier>> {
         override fun onNewData(rawCameraIdentifiers: List<CameraIdentifier>?) {
             if (!isMonitoring.get()) return
-            val factory = cameraFactory ?: return
 
-            // Phase 1: Update CameraFactory
+            val factory = cameraFactory ?: return
+            val repo = cameraRepository ?: return
+            val validator = cameraValidator ?: return
+
             val rawIdStrings = rawCameraIdentifiers?.map { it.internalId } ?: emptyList()
+
+            // For factories that support interrogation, we can pre-validate the change.
+            if (factory is CameraFactory.Interrogator) {
+                val oldFilteredIds = currentFilteredIds
+                val potentialNewIds =
+                    factory.getAvailableCameraIds(rawIdStrings).map { CameraIdentifier.create(it) }
+
+                val removedCameras = oldFilteredIds.toSet() - potentialNewIds.toSet()
+                if (
+                    removedCameras.isNotEmpty() &&
+                        validator.isChangeInvalid(repo.cameras, removedCameras)
+                ) {
+                    Logger.w(TAG, "Camera removal update invalid. Aborting.")
+                    return
+                }
+            }
+
+            // After any pre-validation has passed, commit the update to the factory.
             try {
                 factory.onCameraIdsUpdated(rawIdStrings)
             } catch (e: Exception) {
-                Logger.e(TAG, "CameraFactory failed to update. Triggering refresh.", e)
-                sourcePresenceObservable?.fetchData()
+                Logger.w(
+                    TAG,
+                    "CameraFactory failed to update. The camera list may be stale until the next update.",
+                    e,
+                )
                 return
             }
 
-            // Phase 2: Get filtered list and start transaction
+            // Now, get the definitive new list from the factory's updated state.
             val newFilteredIds = factory.availableCameraIds.map { CameraIdentifier.create(it) }
+
+            // If the final list results in no change, we can stop.
+            if (newFilteredIds == currentFilteredIds) {
+                return
+            }
+
+            // Proceed with the full update transaction.
             processFilteredCameraIdUpdate(newFilteredIds)
         }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraValidator.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraValidator.java
deleted file mode 100644
index ce4ef85..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraValidator.java
+++ /dev/null
@@ -1,167 +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.camera.core.impl;
-
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.os.Build;
-
-import androidx.annotation.OptIn;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.CameraSelector;
-import androidx.camera.core.ExperimentalLensFacing;
-import androidx.camera.core.Logger;
-
-import org.jspecify.annotations.NonNull;
-import org.jspecify.annotations.Nullable;
-
-import java.util.Set;
-
-/**
- * Validation methods to verify the camera is initialized successfully, more info please reference
- * b/167201193.
- */
-@OptIn(markerClass = ExperimentalLensFacing.class)
-public final class CameraValidator {
-    private CameraValidator() {
-    }
-
-    private static final String TAG = "CameraValidator";
-    private static final CameraSelector EXTERNAL_LENS_FACING =
-            new CameraSelector.Builder().requireLensFacing(
-                    CameraSelector.LENS_FACING_EXTERNAL).build();
-
-    /**
-     * Verifies the initialized camera instance in the CameraRepository
-     *
-     * <p>It should initialize the cameras that physically supported on the device. The
-     * physically supported device lens facing information comes from the package manager and the
-     * system feature flags are set by the vendor as part of the device build and CTS verified.
-     *
-     * @param context                  The application or activity context.
-     * @param cameraRepository         The camera repository for verify.
-     * @param availableCamerasSelector Indicate the camera that we need to check.
-     * @throws CameraIdListIncorrectException if it fails to find all the camera instances that
-     *                                        physically supported on the device.
-     */
-    public static void validateCameras(@NonNull Context context,
-            @NonNull CameraRepository cameraRepository,
-            @Nullable CameraSelector availableCamerasSelector)
-            throws CameraIdListIncorrectException {
-
-        // Check if running on a virtual device with Android U or higher
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
-                && Api34Impl.getDeviceId(context) != Context.DEVICE_ID_DEFAULT) {
-
-            Set<CameraInternal> availableCameras = cameraRepository.getCameras();
-            // Log details and skip validation since we have at least one camera
-            Logger.d(TAG, "Virtual device with ID: " + Api34Impl.getDeviceId(context)
-                    + " has " + availableCameras.size() + " cameras. Skipping validation.");
-            return;
-        }
-
-        Integer lensFacing = null;
-        try {
-            if (availableCamerasSelector != null
-                    && (lensFacing = availableCamerasSelector.getLensFacing()) == null) {
-                Logger.w(TAG, "No lens facing info in the availableCamerasSelector, don't "
-                        + "verify the camera lens facing.");
-                return;
-            }
-        } catch (IllegalStateException e) {
-            Logger.e(TAG, "Cannot get lens facing from the availableCamerasSelector don't "
-                    + "verify the camera lens facing.", e);
-            return;
-        }
-
-        Logger.d(TAG,
-                "Verifying camera lens facing on " + Build.DEVICE + ", lensFacingInteger: "
-                        + lensFacing);
-
-        PackageManager pm = context.getPackageManager();
-        Throwable exception = null;
-        int availableCameraCount = 0;
-        try {
-            if (pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
-                if (availableCamerasSelector == null
-                        || lensFacing.intValue() == CameraSelector.LENS_FACING_BACK) {
-                    // Only verify the main camera if it is NOT specifying the available lens
-                    // facing or it required the LENS_FACING_BACK camera.
-                    CameraSelector.DEFAULT_BACK_CAMERA.select(cameraRepository.getCameras());
-                    availableCameraCount++;
-                }
-            }
-        } catch (IllegalArgumentException e) {
-            Logger.w(TAG, "Camera LENS_FACING_BACK verification failed", e);
-            exception = e;
-        }
-        try {
-            if (pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)) {
-                if (availableCamerasSelector == null
-                        || lensFacing.intValue() == CameraSelector.LENS_FACING_FRONT) {
-                    // Only verify the front camera if it is NOT specifying the available lens
-                    // facing or it required the LENS_FACING_FRONT camera.
-                    CameraSelector.DEFAULT_FRONT_CAMERA.select(cameraRepository.getCameras());
-                    availableCameraCount++;
-                }
-            }
-        } catch (IllegalArgumentException e) {
-            Logger.w(TAG, "Camera LENS_FACING_FRONT verification failed", e);
-            exception = e;
-        }
-        try {
-            // Verifies the EXTERNAL camera.
-            EXTERNAL_LENS_FACING.select(cameraRepository.getCameras());
-            Logger.d(TAG, "Found a LENS_FACING_EXTERNAL camera");
-            availableCameraCount++;
-        } catch (IllegalArgumentException e) {
-        }
-
-        if (exception != null) {
-            Logger.e(TAG, "Camera LensFacing verification failed, existing cameras: "
-                    + cameraRepository.getCameras());
-            throw new CameraIdListIncorrectException(
-                    "Expected camera missing from device.", availableCameraCount, exception);
-        }
-    }
-
-    /** The exception for the b/167201193: incorrect camera id list. */
-    public static class CameraIdListIncorrectException extends Exception {
-
-        private final int mAvailableCameraCount;
-
-        public CameraIdListIncorrectException(@Nullable String message,
-                int availableCameraCount, @Nullable Throwable cause) {
-            super(message, cause);
-            mAvailableCameraCount = availableCameraCount;
-        }
-
-        public int getAvailableCameraCount() {
-            return mAvailableCameraCount;
-        }
-    }
-
-    @RequiresApi(34)
-    private static class Api34Impl {
-        private Api34Impl() {
-        }
-
-        static int getDeviceId(@NonNull Context context) {
-            return context.getDeviceId();
-        }
-    }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraValidator.kt b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraValidator.kt
new file mode 100644
index 0000000..1f43bf6
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraValidator.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.core.CameraIdentifier
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.Logger
+
+/** An interface for validating the camera state against system-advertised features. */
+public interface CameraValidator {
+
+    /**
+     * Validates the initial set of cameras upon startup.
+     *
+     * @throws CameraIdListIncorrectException if a camera required by system features is missing.
+     */
+    @Throws(CameraIdListIncorrectException::class)
+    public fun validateOnFirstInit(cameraRepository: CameraRepository)
+
+    /**
+     * Checks if a proposed camera removal would result in a degraded state.
+     *
+     * This method determines if removing a set of cameras would violate the device's hardware
+     * contract as defined by system features (e.g., `PackageManager.FEATURE_CAMERA`). A change is
+     * considered **invalid** if it removes a **required** camera that is **currently available**,
+     * thereby making the camera state "worse".
+     *
+     * **Examples (Assuming device requires a BACK and FRONT camera):**
+     * 1. **Valid:** Removing a non-required `EXTERNAL` camera from `[BACK, FRONT, EXTERNAL]` is
+     *    allowed.
+     * 2. **Invalid:** Removing the required `BACK` camera from `[BACK, FRONT]` is blocked.
+     * 3. **Valid (Degraded State):** If the initial state is `[BACK, EXTERNAL]` (missing the
+     *    required `FRONT`), removing the non-required `EXTERNAL` camera is still allowed as the
+     *    situation does not worsen.
+     *
+     * The validation logic is constrained by the `availableCamerasSelector` provided when this
+     * validator was created.
+     *
+     * @param currentCameras The complete set of `CameraInternal` objects available *before* the
+     *   proposed removal.
+     * @param removedCameras The set of `CameraIdentifier`s for the cameras that are being removed.
+     * @return `true` if the change is invalid and should be aborted, `false` otherwise.
+     */
+    public fun isChangeInvalid(
+        currentCameras: Set<CameraInternal>,
+        removedCameras: Set<CameraIdentifier>,
+    ): Boolean
+
+    /** The exception for an incorrect camera id list. */
+    public class CameraIdListIncorrectException(
+        message: String?,
+        public val availableCameraCount: Int,
+        cause: Throwable?,
+    ) : Exception(message, cause)
+
+    /** Companion object to hold the factory method. */
+    public companion object {
+        /**
+         * Creates a new instance of the default CameraValidator.
+         *
+         * @param context The application context.
+         * @param availableCamerasSelector The selector that filters which cameras to validate.
+         * @return A new [CameraValidator] instance.
+         */
+        @JvmStatic
+        public // Makes this callable as a static method from Java
+        fun create(context: Context, availableCamerasSelector: CameraSelector?): CameraValidator {
+            return CameraValidatorImpl(context, availableCamerasSelector)
+        }
+    }
+}
+
+/**
+ * The default implementation of [CameraValidator].
+ *
+ * This validator is configured with a context and an optional camera selector upon creation.
+ */
+public class CameraValidatorImpl(
+    private val context: Context,
+    private val availableCamerasSelector: CameraSelector?,
+) : CameraValidator {
+
+    private val isVirtualDevice = isVirtualDevice(context)
+    private val validationCriteria = getValidationCriteria()
+
+    override fun validateOnFirstInit(cameraRepository: CameraRepository) {
+        if (isVirtualDevice) {
+            Logger.d(
+                TAG,
+                "Virtual device with " +
+                    "${cameraRepository.cameras.size} cameras. Skipping validation.",
+            )
+            return
+        }
+
+        Logger.d(TAG, "Verifying camera lens facing on " + Build.DEVICE)
+        var exception: RuntimeException? = null
+
+        if (validationCriteria.checkBack) {
+            try {
+                CameraSelector.DEFAULT_BACK_CAMERA.select(cameraRepository.cameras)
+            } catch (e: RuntimeException) {
+                Logger.w(TAG, "Camera LENS_FACING_BACK verification failed", e)
+                exception = e
+            }
+        }
+
+        if (validationCriteria.checkFront) {
+            try {
+                CameraSelector.DEFAULT_FRONT_CAMERA.select(cameraRepository.cameras)
+            } catch (e: RuntimeException) {
+                Logger.w(TAG, "Camera LENS_FACING_FRONT verification failed", e)
+                if (exception == null) exception = e
+            }
+        }
+
+        if (exception != null) {
+            throw CameraValidator.CameraIdListIncorrectException(
+                "Expected camera missing from device.",
+                cameraRepository.cameras.size,
+                exception,
+            )
+        }
+    }
+
+    override fun isChangeInvalid(
+        currentCameras: Set<CameraInternal>,
+        removedCameras: Set<CameraIdentifier>,
+    ): Boolean {
+        if (isVirtualDevice || (!validationCriteria.checkBack && !validationCriteria.checkFront)) {
+            return false
+        }
+
+        val hadBack = hasCamera(currentCameras, CameraSelector.DEFAULT_BACK_CAMERA)
+        val hadFront = hasCamera(currentCameras, CameraSelector.DEFAULT_FRONT_CAMERA)
+
+        val removedCameraIds = removedCameras.map { it.internalId }.toSet()
+        val newProposedCameras =
+            currentCameras.filter { it.cameraInfoInternal.cameraId !in removedCameraIds }.toSet()
+
+        val willHaveBack = hasCamera(newProposedCameras, CameraSelector.DEFAULT_BACK_CAMERA)
+        val willHaveFront = hasCamera(newProposedCameras, CameraSelector.DEFAULT_FRONT_CAMERA)
+
+        val backCameraIsLost = validationCriteria.checkBack && hadBack && !willHaveBack
+        val frontCameraIsLost = validationCriteria.checkFront && hadFront && !willHaveFront
+
+        return backCameraIsLost || frontCameraIsLost
+    }
+
+    private fun hasCamera(cameras: Set<CameraInternal>, selector: CameraSelector): Boolean {
+        return try {
+            selector.select(LinkedHashSet(cameras))
+            true
+        } catch (_: IllegalArgumentException) {
+            false
+        }
+    }
+
+    private data class ValidationCriteria(val checkBack: Boolean, val checkFront: Boolean)
+
+    private fun getValidationCriteria(): ValidationCriteria {
+        val pm = context.packageManager
+        val lensFacing = this.availableCamerasSelector?.lensFacing
+        val needsBackCamera = pm.hasSystemFeature(PackageManager.FEATURE_CAMERA)
+        val needsFrontCamera = pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT)
+
+        val checkBack =
+            needsBackCamera && (lensFacing == null || lensFacing == CameraSelector.LENS_FACING_BACK)
+        val checkFront =
+            needsFrontCamera &&
+                (lensFacing == null || lensFacing == CameraSelector.LENS_FACING_FRONT)
+        return ValidationCriteria(checkBack, checkFront)
+    }
+
+    private fun isVirtualDevice(context: Context): Boolean {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
+            Api34Impl.getDeviceId(context) != Context.DEVICE_ID_DEFAULT
+    }
+
+    @RequiresApi(34)
+    private object Api34Impl {
+        fun getDeviceId(context: Context): Int {
+            return context.getDeviceId()
+        }
+    }
+
+    public companion object {
+        private const val TAG = "CameraValidator"
+    }
+}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraPresenceProviderTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraPresenceProviderTest.kt
index b6f718c..8af0795 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraPresenceProviderTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraPresenceProviderTest.kt
@@ -45,6 +45,7 @@
     private lateinit var testCameraRepository: TestCameraRepository
     private val fakeCoordinator = FakeCameraCoordinator()
     private val fakeSurfaceManager = FakeCameraDeviceSurfaceManager()
+    private val fakeValidator = FakeCameraValidator()
     private val publicListener = TestCameraPresenceListener()
 
     private lateinit var provider: CameraPresenceProvider
@@ -58,13 +59,103 @@
         testCameraRepository = TestCameraRepository(fakeCameraFactory)
 
         provider = CameraPresenceProvider(MoreExecutors.directExecutor())
-        provider.startup(fakeCameraFactory, testCameraRepository)
+        provider.startup(fakeValidator, fakeCameraFactory, testCameraRepository)
         provider.addDependentInternalListener(fakeSurfaceManager)
         provider.addDependentInternalListener(fakeCoordinator)
         provider.addCameraPresenceListener(publicListener, MoreExecutors.directExecutor())
     }
 
     @Test
+    fun onNewData_abortsUpdate_whenValidatorReturnsInvalid() {
+        // Arrange: Start with one camera.
+        fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK, CAMERA_ID_0) {
+            FakeCamera(CAMERA_ID_0, null, FakeCameraInfoInternal())
+        }
+        sourceObservable.updateData(listOf(IDENTIFIER_0))
+
+        // Sanity check initial state.
+        assertThat(testCameraRepository.updateCount).isEqualTo(1)
+        assertThat(testCameraRepository.lastReceivedIds).containsExactly(CAMERA_ID_0)
+
+        // Act: Configure validator to fail the next update.
+        fakeValidator.setNextIsChangeInvalid(true)
+        // Attempt to remove the camera.
+        sourceObservable.updateData(emptyList())
+
+        // Assert: The update should have been aborted.
+        // The repository update count remains 1 because the invalid update was blocked.
+        assertThat(testCameraRepository.updateCount).isEqualTo(1)
+        // The repository state should not have changed.
+        assertThat(testCameraRepository.lastReceivedIds).containsExactly(CAMERA_ID_0)
+        // The public listener should not be notified of any removal.
+        assertThat(publicListener.removedCameras).isEmpty()
+    }
+
+    @Test
+    fun onNewData_allowsNonImpactfulRemoval_fromDegradedState() {
+        // Arrange: Start in a degraded state with a required BACK camera and an EXTERNAL camera,
+        // but missing the required FRONT camera.
+        fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK, CAMERA_ID_0) {
+            FakeCamera(
+                CAMERA_ID_0,
+                null,
+                FakeCameraInfoInternal(CAMERA_ID_0, 0, CameraSelector.LENS_FACING_BACK),
+            )
+        }
+        fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_EXTERNAL, CAMERA_ID_EXTERNAL) {
+            FakeCamera(
+                CAMERA_ID_EXTERNAL,
+                null,
+                FakeCameraInfoInternal(CAMERA_ID_EXTERNAL, 0, CameraSelector.LENS_FACING_EXTERNAL),
+            )
+        }
+        sourceObservable.updateData(listOf(IDENTIFIER_0, IDENTIFIER_EXTERNAL))
+        assertThat(testCameraRepository.updateCount).isEqualTo(1)
+
+        // Act: Remove the non-essential EXTERNAL camera. The validator will allow this
+        // by default (isChangeInvalid returns false).
+        fakeCameraFactory.removeCamera(CAMERA_ID_EXTERNAL)
+        sourceObservable.updateData(listOf(IDENTIFIER_0))
+
+        // Assert: The update proceeds because it doesn't make the state worse.
+        assertThat(testCameraRepository.updateCount).isEqualTo(2)
+        assertThat(testCameraRepository.lastReceivedIds).containsExactly(CAMERA_ID_0)
+        assertThat(publicListener.removedCameras).containsExactly(IDENTIFIER_EXTERNAL)
+    }
+
+    @Test
+    fun onNewData_blocksImpactfulRemoval_fromDegradedState() {
+        // Arrange: Start in a degraded state with a required BACK camera and an EXTERNAL camera.
+        fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK, CAMERA_ID_0) {
+            FakeCamera(
+                CAMERA_ID_0,
+                null,
+                FakeCameraInfoInternal(CAMERA_ID_0, 0, CameraSelector.LENS_FACING_BACK),
+            )
+        }
+        fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_EXTERNAL, CAMERA_ID_EXTERNAL) {
+            FakeCamera(
+                CAMERA_ID_EXTERNAL,
+                null,
+                FakeCameraInfoInternal(CAMERA_ID_EXTERNAL, 0, CameraSelector.LENS_FACING_EXTERNAL),
+            )
+        }
+        sourceObservable.updateData(listOf(IDENTIFIER_0, IDENTIFIER_EXTERNAL))
+        assertThat(testCameraRepository.updateCount).isEqualTo(1)
+
+        // Act: Configure the validator to block the next removal, then attempt to remove the
+        // last required BACK camera.
+        fakeValidator.setNextIsChangeInvalid(true)
+        sourceObservable.updateData(listOf(IDENTIFIER_EXTERNAL))
+
+        // Assert: The update is blocked to prevent losing the last required camera.
+        assertThat(testCameraRepository.updateCount).isEqualTo(1)
+        assertThat(testCameraRepository.lastReceivedIds)
+            .containsExactly(CAMERA_ID_0, CAMERA_ID_EXTERNAL)
+        assertThat(publicListener.removedCameras).isEmpty()
+    }
+
+    @Test
     fun startup_notifiesPublicListenerOfInitialCameras() {
         // Arrange: Initial state in factory
         fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK, CAMERA_ID_0) {
@@ -250,8 +341,10 @@
     private companion object {
         private const val CAMERA_ID_0 = "0"
         private const val CAMERA_ID_1 = "1"
+        private const val CAMERA_ID_EXTERNAL = "2"
         private val IDENTIFIER_0 = CameraIdentifier.create(CAMERA_ID_0)
         private val IDENTIFIER_1 = CameraIdentifier.create(CAMERA_ID_1)
+        private val IDENTIFIER_EXTERNAL = CameraIdentifier.create(CAMERA_ID_EXTERNAL)
     }
 
     private class MutableObservable<T> : Observable<T> {
@@ -315,4 +408,27 @@
             this.shouldThrow = shouldThrow
         }
     }
+
+    /** A fake implementation of CameraValidator for testing purposes. */
+    private class FakeCameraValidator : CameraValidator {
+        private var nextIsChangeInvalid = false
+
+        fun setNextIsChangeInvalid(isInvalid: Boolean) {
+            nextIsChangeInvalid = isInvalid
+        }
+
+        override fun validateOnFirstInit(cameraRepository: CameraRepository) {
+            // No-op for these tests, as we are not testing initial validation here.
+        }
+
+        override fun isChangeInvalid(
+            currentCameras: Set<CameraInternal>,
+            removedCameras: Set<CameraIdentifier>,
+        ): Boolean {
+            val result = nextIsChangeInvalid
+            // Reset after use so the next check defaults to valid.
+            nextIsChangeInvalid = false
+            return result
+        }
+    }
 }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraValidatorTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraValidatorTest.kt
new file mode 100644
index 0000000..256ca8b
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/CameraValidatorTest.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.content.Context
+import android.content.pm.PackageManager
+import androidx.camera.core.CameraIdentifier
+import androidx.camera.core.CameraSelector
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraInfoInternal
+import androidx.camera.testing.impl.fakes.FakeCameraFactory
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+class CameraValidatorTest {
+
+    private lateinit var context: Context
+    private lateinit var packageManager: PackageManager
+    private lateinit var fakeCameraFactory: FakeCameraFactory
+    private lateinit var cameraRepository: CameraRepository
+
+    @Before
+    fun setUp() {
+        context = ApplicationProvider.getApplicationContext()
+        packageManager = context.packageManager
+        fakeCameraFactory = FakeCameraFactory()
+        cameraRepository = CameraRepository()
+    }
+
+    @Test
+    fun validateOnFirstInit_succeeds_whenAllRequiredCamerasExist() {
+        // Arrange
+        setSystemFeatures(hasBackCamera = true, hasFrontCamera = true)
+        setupCameras(hasBack = true, hasFront = true)
+        val validator = CameraValidator.create(context, null)
+
+        // Act & Assert
+        // Should not throw any exception
+        validator.validateOnFirstInit(cameraRepository)
+    }
+
+    @Test
+    fun validateOnFirstInit_throwsException_whenBackCameraIsMissing() {
+        // Arrange
+        setSystemFeatures(hasBackCamera = true, hasFrontCamera = false)
+        setupCameras(hasBack = false, hasFront = true) // Missing required back camera
+        val validator = CameraValidator.create(context, null)
+
+        // Act & Assert
+        assertThrows(CameraValidator.CameraIdListIncorrectException::class.java) {
+            validator.validateOnFirstInit(cameraRepository)
+        }
+    }
+
+    @Test
+    fun validateOnFirstInit_throwsException_whenFrontCameraIsMissing() {
+        // Arrange
+        setSystemFeatures(hasBackCamera = true, hasFrontCamera = true)
+        setupCameras(hasBack = true, hasFront = false) // Missing required front camera
+        val validator = CameraValidator.create(context, null)
+
+        // Act & Assert
+        assertThrows(CameraValidator.CameraIdListIncorrectException::class.java) {
+            validator.validateOnFirstInit(cameraRepository)
+        }
+    }
+
+    @Test
+    fun validateOnFirstInit_succeeds_whenMissingCameraIsNotInSelectorScope() {
+        // Arrange: System requires both, but repository only has a BACK camera.
+        setSystemFeatures(hasBackCamera = true, hasFrontCamera = true)
+        setupCameras(hasBack = true, hasFront = false)
+        // Act: Create a validator that ONLY cares about the back camera.
+        val validator = CameraValidator.create(context, CameraSelector.DEFAULT_BACK_CAMERA)
+
+        // Assert: No exception, because the missing FRONT camera is out of scope.
+        validator.validateOnFirstInit(cameraRepository)
+    }
+
+    @Test
+    fun validateOnFirstInit_reportsCorrectCameraCount_onFailure() {
+        // Arrange: System requires a back camera, but it's missing. One front camera exists.
+        setSystemFeatures(hasBackCamera = true, hasFrontCamera = false)
+        setupCameras(hasBack = false, hasFront = true)
+        val validator = CameraValidator.create(context, null)
+
+        // Act: Catch the exception
+        val exception =
+            assertThrows(CameraValidator.CameraIdListIncorrectException::class.java) {
+                validator.validateOnFirstInit(cameraRepository)
+            }
+
+        // Assert: The exception correctly reports that 1 camera was still available,
+        // which allows recovery logic to proceed with a degraded state.
+        assertThat(exception.availableCameraCount).isEqualTo(1)
+    }
+
+    @Test
+    fun isChangeInvalid_returnsFalse_forRemovingNonRequiredCamera() {
+        // Arrange
+        setSystemFeatures(hasBackCamera = true, hasFrontCamera = true)
+        val cameras = setupCameras(hasBack = true, hasFront = true, hasExternal = true)
+        val validator = CameraValidator.create(context, null)
+
+        // Act: Remove the external camera
+        val isInvalid = validator.isChangeInvalid(cameras, setOf(IDENTIFIER_EXTERNAL))
+
+        // Assert
+        assertThat(isInvalid).isFalse()
+    }
+
+    @Test
+    fun isChangeInvalid_returnsTrue_forRemovingRequiredBackCamera() {
+        // Arrange
+        setSystemFeatures(hasBackCamera = true, hasFrontCamera = true)
+        val cameras = setupCameras(hasBack = true, hasFront = true)
+        val validator = CameraValidator.create(context, null)
+
+        // Act: Remove the BACK camera
+        val isInvalid = validator.isChangeInvalid(cameras, setOf(IDENTIFIER_BACK))
+
+        // Assert
+        assertThat(isInvalid).isTrue()
+    }
+
+    @Test
+    fun isChangeInvalid_returnsFalse_whenMissingCameraIsNotInSelectorScope() {
+        // Arrange
+        setSystemFeatures(hasBackCamera = true, hasFrontCamera = true)
+        val cameras = setupCameras(hasBack = true, hasFront = true)
+        // This validator only cares about the BACK camera.
+        val validator = CameraValidator.create(context, CameraSelector.DEFAULT_BACK_CAMERA)
+
+        // Act: Remove the FRONT camera.
+        val isInvalid = validator.isChangeInvalid(cameras, setOf(IDENTIFIER_FRONT))
+
+        // Assert: Change is valid because the removed camera was out of scope.
+        assertThat(isInvalid).isFalse()
+    }
+
+    @Test
+    fun isChangeInvalid_allowsNonRequiredRemoval_fromDegradedState() {
+        // Arrange: System requires BACK and FRONT, but we start in a degraded state
+        // with only BACK and an EXTERNAL camera (e.g. after a partial init).
+        setSystemFeatures(hasBackCamera = true, hasFrontCamera = true)
+        val cameras = setupCameras(hasBack = true, hasFront = false, hasExternal = true)
+        val validator = CameraValidator.create(context, null)
+
+        // Act: Remove the EXTERNAL camera.
+        val isInvalid = validator.isChangeInvalid(cameras, setOf(IDENTIFIER_EXTERNAL))
+
+        // Assert: This change is NOT invalid because we didn't lose a *required* camera
+        // that we actually had. The state did not become worse.
+        assertThat(isInvalid).isFalse()
+    }
+
+    private fun setSystemFeatures(hasBackCamera: Boolean, hasFrontCamera: Boolean) {
+        val shadowPackageManager = Shadows.shadowOf(packageManager)
+        shadowPackageManager.setSystemFeature(PackageManager.FEATURE_CAMERA, hasBackCamera)
+        shadowPackageManager.setSystemFeature(PackageManager.FEATURE_CAMERA_FRONT, hasFrontCamera)
+    }
+
+    private fun setupCameras(
+        hasBack: Boolean = false,
+        hasFront: Boolean = false,
+        hasExternal: Boolean = false,
+    ): Set<CameraInternal> {
+        if (hasBack) {
+            fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK, CAMERA_ID_BACK) {
+                FakeCamera(
+                    CAMERA_ID_BACK,
+                    null,
+                    FakeCameraInfoInternal(CAMERA_ID_BACK, 0, CameraSelector.LENS_FACING_BACK),
+                )
+            }
+        }
+        if (hasFront) {
+            fakeCameraFactory.insertCamera(CameraSelector.LENS_FACING_FRONT, CAMERA_ID_FRONT) {
+                FakeCamera(
+                    CAMERA_ID_FRONT,
+                    null,
+                    FakeCameraInfoInternal(CAMERA_ID_FRONT, 0, CameraSelector.LENS_FACING_FRONT),
+                )
+            }
+        }
+        if (hasExternal) {
+            fakeCameraFactory.insertCamera(
+                CameraSelector.LENS_FACING_EXTERNAL,
+                CAMERA_ID_EXTERNAL,
+            ) {
+                FakeCamera(
+                    CAMERA_ID_EXTERNAL,
+                    null,
+                    FakeCameraInfoInternal(
+                        CAMERA_ID_EXTERNAL,
+                        0,
+                        CameraSelector.LENS_FACING_EXTERNAL,
+                    ),
+                )
+            }
+        }
+        cameraRepository.init(fakeCameraFactory)
+        return cameraRepository.cameras
+    }
+
+    private companion object {
+        private const val CAMERA_ID_BACK = "0"
+        private const val CAMERA_ID_FRONT = "1"
+        private const val CAMERA_ID_EXTERNAL = "2"
+
+        private val IDENTIFIER_BACK = CameraIdentifier.create(CAMERA_ID_BACK)
+        private val IDENTIFIER_FRONT = CameraIdentifier.create(CAMERA_ID_FRONT)
+        private val IDENTIFIER_EXTERNAL = CameraIdentifier.create(CAMERA_ID_EXTERNAL)
+    }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraFactory.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraFactory.java
index e16cc31..2743fd0 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraFactory.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeCameraFactory.java
@@ -27,11 +27,13 @@
 import androidx.camera.core.concurrent.CameraCoordinator;
 import androidx.camera.core.impl.CameraFactory;
 import androidx.camera.core.impl.CameraInternal;
-import androidx.camera.core.impl.ConstantObservable;
 import androidx.camera.core.impl.Observable;
 import androidx.core.util.Pair;
 import androidx.core.util.Preconditions;
 
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
 import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
@@ -42,15 +44,17 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
 
 /**
  * A {@link CameraFactory} implementation that contains and produces fake cameras.
  *
  */
 @RestrictTo(Scope.LIBRARY_GROUP)
-public final class FakeCameraFactory implements CameraFactory {
+public final class FakeCameraFactory implements CameraFactory, CameraFactory.Interrogator {
 
     private static final String TAG = "FakeCameraFactory";
 
@@ -63,7 +67,7 @@
     private @NonNull CameraCoordinator mCameraCoordinator = new FakeCameraCoordinator();
 
     private @NonNull Observable<List<CameraIdentifier>> mCameraSourceObservable =
-            ConstantObservable.withValue(new ArrayList<>());
+            new ControllableObservable();
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     final Map<String, Pair<Integer, Callable<CameraInternal>>> mCameraMap = new HashMap<>();
@@ -74,6 +78,8 @@
 
     public FakeCameraFactory(@Nullable CameraSelector availableCamerasSelector) {
         mAvailableCamerasSelector = availableCamerasSelector;
+
+        updateCameraPresence();
     }
 
     @Override
@@ -104,6 +110,8 @@
         mCachedCameraIds = null;
 
         mCameraMap.put(cameraId, Pair.create(lensFacing, cameraInternal));
+
+        updateCameraPresence();
     }
 
     /**
@@ -152,6 +160,9 @@
 
         // Remove from the map and return the old value.
         Pair<Integer, Callable<CameraInternal>> removed = mCameraMap.remove(cameraId);
+
+        updateCameraPresence();
+
         if (removed != null) {
             return removed.second;
         }
@@ -166,19 +177,42 @@
             if (mAvailableCamerasSelector == null) {
                 mCachedCameraIds = Collections.unmodifiableSet(new HashSet<>(mCameraMap.keySet()));
             } else {
-                mCachedCameraIds = Collections.unmodifiableSet(new HashSet<>(filteredCameraIds()));
+                mCachedCameraIds = Collections.unmodifiableSet(
+                        new HashSet<>(filterCameraIds(mCameraMap.keySet())));
             }
         }
         return mCachedCameraIds;
     }
 
-    /** Returns a list of camera ids filtered with {@link #mAvailableCamerasSelector}. */
-    private @NonNull List<String> filteredCameraIds() {
+    @NonNull
+    @Override
+    public List<String> getAvailableCameraIds(@NonNull List<String> cameraIds) {
+        if (mAvailableCamerasSelector == null) {
+            // No selector, just return the input list but ensure cameras exist in our map.
+            List<String> existingIds = new ArrayList<>();
+            for (String cameraId : cameraIds) {
+                if (mCameraMap.containsKey(cameraId)) {
+                    existingIds.add(cameraId);
+                }
+            }
+            return existingIds;
+        }
+        return filterCameraIds(cameraIds);
+    }
+
+    /**
+     * A private helper to apply the CameraSelector filter to any list of camera IDs.
+     * This is used by both getAvailableCameraIds() and the new Interrogator method.
+     */
+    private @NonNull List<String> filterCameraIds(@NonNull Iterable<String> cameraIds) {
         Preconditions.checkNotNull(mAvailableCamerasSelector);
         final List<String> filteredCameraIds = new ArrayList<>();
-        for (Map.Entry<String, Pair<Integer, Callable<CameraInternal>>> entry :
-                mCameraMap.entrySet()) {
-            final Callable<CameraInternal> callable = entry.getValue().second;
+        for (String cameraId : cameraIds) {
+            if (!mCameraMap.containsKey(cameraId)) {
+                continue;
+            }
+            final Callable<CameraInternal> callable =
+                    Objects.requireNonNull(mCameraMap.get(cameraId)).second;
             if (callable == null) {
                 continue;
             }
@@ -188,7 +222,7 @@
                         mAvailableCamerasSelector.filter(
                                 new LinkedHashSet<>(Collections.singleton(camera)));
                 if (!filteredCameraInternals.isEmpty()) {
-                    filteredCameraIds.add(entry.getKey());
+                    filteredCameraIds.add(cameraId);
                 }
             } catch (Exception exception) {
                 Logger.e(TAG, "Failed to get access to the camera instance.", exception);
@@ -229,4 +263,58 @@
     public void onCameraIdsUpdated(@NonNull List<String> cameraIds) {
 
     }
+
+    /**
+     * A new private helper to push updates to the camera presence observable.
+     */
+    private void updateCameraPresence() {
+        Set<String> availableIds = getAvailableCameraIds();
+        List<CameraIdentifier> identifiers = new ArrayList<>();
+        for (String id : availableIds) {
+            identifiers.add(CameraIdentifier.create(id));
+        }
+
+        // This check is needed because setCameraPresenceSource can overwrite our observable.
+        if (mCameraSourceObservable instanceof ControllableObservable) {
+            ((ControllableObservable) mCameraSourceObservable).updateData(identifiers);
+        }
+    }
+
+    /**
+     * A simple observable implementation that allows internal updates.
+     */
+    private static class ControllableObservable implements Observable<List<CameraIdentifier>> {
+        private Observer<? super List<CameraIdentifier>> mObserver;
+        private Executor mExecutor;
+        private List<CameraIdentifier> mData = new ArrayList<>();
+
+        @Override
+        public void addObserver(@NonNull Executor executor,
+                @NonNull Observer<? super List<CameraIdentifier>> observer) {
+            mExecutor = executor;
+            mObserver = observer;
+            updateData(mData);
+        }
+
+        @Override
+        public void removeObserver(@NonNull Observer<? super List<CameraIdentifier>> observer) {
+            if (Objects.equals(mObserver, observer)) {
+                mObserver = null;
+                mExecutor = null;
+            }
+        }
+
+        @Override
+        public @NonNull ListenableFuture<List<CameraIdentifier>> fetchData() {
+            return Futures.immediateFuture(mData);
+        }
+
+        void updateData(@NonNull List<CameraIdentifier> data) {
+            mData = data;
+            if (mExecutor != null && mObserver != null) {
+                Observer<? super List<CameraIdentifier>> observer = mObserver;
+                mExecutor.execute(() -> observer.onNewData(data));
+            }
+        }
+    }
 }
diff --git a/collection/collection/build.gradle b/collection/collection/build.gradle
index 5906650..e31fa5c 100644
--- a/collection/collection/build.gradle
+++ b/collection/collection/build.gradle
@@ -61,12 +61,7 @@
         commonTest {
             dependencies {
                 implementation(libs.kotlinTestAnnotationsCommon)
-                // TODO(b/375358921) Set to libs.kotlinTest when collection targets Kotlin 2
-                // For KotlinWasm, versions of toolchain and stdlib need to be the same for tests:
-                // https://youtrack.jetbrains.com/issue/KT-71032
-                // So we force kotlin-test to be on Kotlin 2.
-                // Published artifacts are unaffected by this, they continue to target Kotlin 1.9
-                implementation(libs.kotlinTestForWasmTests)
+                implementation(libs.kotlinTest)
                 implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
                 implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
             }
@@ -133,9 +128,6 @@
         wasmJsMain {
             dependsOn(nonJsMain)
             dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
         }
 
         wasmJsTest {
@@ -145,9 +137,6 @@
 
         jsMain {
             dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
         }
 
         jsTest {
@@ -157,15 +146,11 @@
         targets.configureEach { target ->
             if (target.platformType == KotlinPlatformType.native) {
                 target.compilations["main"].defaultSourceSet {
-                    def konanTargetFamily = target.konanTarget.family
-                    if (konanTargetFamily == Family.OSX ||
-                            konanTargetFamily == Family.IOS ||
-                            konanTargetFamily == Family.WATCHOS ||
-                            konanTargetFamily == Family.TVOS) {
+                    if (target.konanTarget.family.appleFamily) {
                         dependsOn(darwinMain)
-                    } else if (konanTargetFamily == Family.LINUX) {
+                    } else if (target.konanTarget.family == Family.LINUX) {
                         dependsOn(linuxMain)
-                    } else if (konanTargetFamily == Family.MINGW) {
+                    } else if (target.konanTarget.family == Family.MINGW) {
                         dependsOn(mingwMain)
                     } else {
                         throw new GradleException("unknown native target ${target}")
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
index f8db0710..9fdaa7d 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/anchoredDraggable/AnchoredDraggableGestureTest.kt
@@ -16,8 +16,11 @@
 
 package androidx.compose.foundation.anchoredDraggable
 
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.core.TwoWayConverter
+import androidx.compose.animation.core.VectorizedAnimationSpec
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.AtomicLong
 import androidx.compose.foundation.AutoTestFrameClock
 import androidx.compose.foundation.anchoredDraggable.AnchoredDraggableTestValue.A
 import androidx.compose.foundation.anchoredDraggable.AnchoredDraggableTestValue.B
@@ -33,7 +36,6 @@
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.MonotonicFrameClock
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.rememberCoroutineScope
@@ -63,7 +65,6 @@
 import kotlin.math.abs
 import kotlin.math.roundToInt
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import org.junit.Test
@@ -823,18 +824,66 @@
         assertThat(state.currentValue).isEqualTo(C)
     }
 
-    private val DefaultSnapAnimationSpec = tween<Float>()
+    @Test
+    fun anchoredDraggable_fling_confirmValueChange_returnsFalse_returnsToSettledAnchor() {
+        val inspectAnimationSpec = InspectSpringAnimationSpec(DefaultSnapAnimationSpec)
+        val (state, modifier) =
+            createStateAndModifier(
+                initialValue = A,
+                Orientation.Horizontal,
+                confirmValueChange = { it != B },
+                snapAnimationSpec = inspectAnimationSpec,
+            )
+        val anchors = DraggableAnchors {
+            A at 0f
+            B at AnchoredDraggableBoxSize.value / 2f
+            C at AnchoredDraggableBoxSize.value
+        }
+        state.updateAnchors(anchors)
 
-    private class HandPumpTestFrameClock : MonotonicFrameClock {
-        private val frameCh = Channel<Long>(1)
-        private val time = AtomicLong(0)
-
-        suspend fun advanceByFrame() {
-            frameCh.send(time.getAndAdd(16_000_000L))
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides NoOpDensity) {
+                WithTouchSlop(0f) {
+                    Box(Modifier.fillMaxSize()) {
+                        Box(
+                            Modifier.requiredSize(AnchoredDraggableBoxSize)
+                                .testTag(AnchoredDraggableTestTag)
+                                .then(modifier)
+                                .offset { IntOffset(state.requireOffset().roundToInt(), 0) }
+                                .background(Color.Red)
+                        )
+                    }
+                }
+            }
         }
 
-        override suspend fun <R> withFrameNanos(onFrame: (frameTimeNanos: Long) -> R): R {
-            return onFrame(frameCh.receive())
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.settledValue).isEqualTo(A)
+
+        rule.onNodeWithTag(AnchoredDraggableTestTag).performTouchInput {
+            swipeRight(endX = right / 2)
+        }
+        assertThat(state.offset).isWithin(0.5f).of(anchors.positionOf(B))
+        rule.waitForIdle()
+
+        assertThat(inspectAnimationSpec.animationWasExecutions).isEqualTo(1)
+        assertThat(state.currentValue).isEqualTo(A)
+        assertThat(state.settledValue).isEqualTo(A)
+        assertThat(state.offset).isEqualTo(anchors.positionOf(A))
+    }
+
+    private val DefaultSnapAnimationSpec = tween<Float>()
+
+    private class InspectSpringAnimationSpec(private val animation: AnimationSpec<Float>) :
+        AnimationSpec<Float> {
+
+        var animationWasExecutions = 0
+
+        override fun <V : AnimationVector> vectorize(
+            converter: TwoWayConverter<Float, V>
+        ): VectorizedAnimationSpec<V> {
+            animationWasExecutions++
+            return animation.vectorize(converter)
         }
     }
 
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerCacheWindowTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerCacheWindowTest.kt
index d76e785..0055610 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerCacheWindowTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerCacheWindowTest.kt
@@ -29,14 +29,12 @@
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.BeforeTest
-import kotlin.test.Ignore
 import kotlinx.coroutines.runBlocking
 import org.junit.Assume
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
-@Ignore
 @OptIn(ExperimentalFoundationApi::class)
 @LargeTest
 @RunWith(Parameterized::class)
@@ -66,7 +64,7 @@
     }
 
     @Test
-    fun noPrefetchingForwardInitially() {
+    fun prefetchingForwardInitially() {
         createPager(
             modifier = Modifier.size(pagesSizeDp * 1.5f),
             pageSize = { PageSize.Fixed(pagesSizeDp) },
@@ -75,10 +73,10 @@
         waitForPrefetch()
         if (config.beyondViewportPageCount == 0) {
             // window will fill automatically 1 extra item
-            rule.onNodeWithTag("2").assertDoesNotExist()
+            rule.onNodeWithTag("2").assertExists()
         } else {
             // window will fill automatically 1 extra item
-            rule.onNodeWithTag("3").assertDoesNotExist()
+            rule.onNodeWithTag("3").assertExists()
         }
     }
 
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerNestedPrefetchingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerNestedPrefetchingTest.kt
index 2215e8a..6263d26 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerNestedPrefetchingTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/pager/PagerNestedPrefetchingTest.kt
@@ -83,13 +83,16 @@
                 Action.Measure(prefetchIndex, 0),
                 Action.Compose(prefetchIndex, 1),
                 Action.Measure(prefetchIndex, 1),
+                Action.Compose(prefetchIndex, 2),
+                Action.Measure(prefetchIndex, 2),
             )
             .inOrder()
 
         rule.onNodeWithTag(tagFor(prefetchIndex)).assertExists()
         rule.onNodeWithTag(tagFor(2, 0)).assertExists()
         rule.onNodeWithTag(tagFor(2, 1)).assertExists()
-        rule.onNodeWithTag(tagFor(2, 2)).assertDoesNotExist()
+        rule.onNodeWithTag(tagFor(2, 2)).assertExists()
+        rule.onNodeWithTag(tagFor(2, 3)).assertDoesNotExist()
     }
 
     @Test
@@ -158,6 +161,8 @@
                 Action.Measure(prefetchIndex, 4),
                 Action.Compose(prefetchIndex, 5),
                 Action.Measure(prefetchIndex, 5),
+                Action.Compose(prefetchIndex, 6),
+                Action.Measure(prefetchIndex, 6),
             )
             .inOrder()
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ComposeFoundationFlags.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ComposeFoundationFlags.kt
index 3a912c8..6fc2d19 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ComposeFoundationFlags.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ComposeFoundationFlags.kt
@@ -147,5 +147,5 @@
      * of 1 item in the direction of the scroll. The window used will be 1 view port AFTER the
      * currently composed items, this includes visible and items composed through beyond bounds.
      */
-    @Suppress("MutableBareField") @JvmField var isCacheWindowForPagerEnabled = false
+    @Suppress("MutableBareField") @JvmField var isCacheWindowForPagerEnabled = true
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index cac5397..fe42aa7 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -1739,14 +1739,20 @@
 
         override fun calculateSnapOffset(velocity: Float): Float {
             val currentOffset = state.requireOffset()
-            val target =
+            val proposedTargetValue =
                 state.anchors.computeTarget(
                     currentOffset = currentOffset,
                     velocity = velocity,
                     positionalThreshold = positionalThreshold,
                     velocityThreshold = velocityThreshold,
                 )
-            return state.anchors.positionOf(target) - currentOffset
+            val targetValue =
+                if (state.confirmValueChange(proposedTargetValue)) {
+                    proposedTargetValue
+                } else {
+                    state.settledValue
+                }
+            return state.anchors.positionOf(targetValue) - currentOffset
         }
     }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/CacheWindowLogic.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/CacheWindowLogic.kt
index 7ebf089..8ce42f1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/CacheWindowLogic.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/CacheWindowLogic.kt
@@ -97,7 +97,7 @@
 
     fun CacheWindowScope.onVisibleItemsUpdated() {
         debugLog { "hasUpdatedVisibleItemsOnce=$hasUpdatedVisibleItemsOnce" }
-        if (!hasUpdatedVisibleItemsOnce && enableInitialPrefetch) {
+        if (!hasUpdatedVisibleItemsOnce) {
             val prefetchForwardWindow =
                 with(cacheWindow) { density?.calculateAheadWindow(mainAxisViewportSize) ?: 0 }
             // we won't fill the window if we don't have a prefetch window
@@ -478,8 +478,6 @@
     val lastVisibleLineIndex: Int
     val mainAxisViewportSize: Int
     val density: Density?
-    val enableInitialPrefetch: Boolean
-        get() = true
 
     fun schedulePrefetch(lineIndex: Int, onItemPrefetched: (Int, Int) -> Unit): List<PrefetchHandle>
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerCacheWindowLogic.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerCacheWindowLogic.kt
index b25ffaa..7f8a510 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerCacheWindowLogic.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerCacheWindowLogic.kt
@@ -28,10 +28,10 @@
 
 @OptIn(ExperimentalFoundationApi::class)
 internal class PagerCacheWindowLogic(
-    viewportFraction: Float,
+    val cacheWindow: LazyLayoutCacheWindow,
     val state: LazyLayoutPrefetchState,
     val itemCount: () -> Int,
-) : CacheWindowLogic(LazyLayoutCacheWindow(viewportFraction, viewportFraction)) {
+) : CacheWindowLogic(cacheWindow) {
     private val cacheWindowScope = PagerCacheWindowScope(itemCount)
 
     fun onScroll(delta: Float, layoutInfo: PagerMeasureResult) {
@@ -105,9 +105,6 @@
     override val density: Density?
         get() = layoutInfo.density
 
-    override val enableInitialPrefetch: Boolean
-        get() = false
-
     override fun schedulePrefetch(
         lineIndex: Int,
         onItemPrefetched: (Int, Int) -> Unit,
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 98f6a6b..322c278 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
@@ -36,6 +36,7 @@
 import androidx.compose.foundation.internal.requirePrecondition
 import androidx.compose.foundation.lazy.layout.AwaitFirstLayoutModifier
 import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo
+import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow
 import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList
 import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState
 import androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope
@@ -351,6 +352,9 @@
     internal val pageSizeWithSpacing: Int
         get() = pageSize + pageSpacing
 
+    // non state backed version
+    internal var latestPageSizeWithSpacing: Int = 0
+
     /**
      * How far the current page needs to scroll so the target page is considered to be the next
      * page.
@@ -458,7 +462,23 @@
             Snapshot.withoutReadObservation { schedulePrecomposition(firstVisiblePage) }
         }
 
-    internal val cacheWindowLogic = PagerCacheWindowLogic(1.0f, prefetchState) { pageCount }
+    /**
+     * Cache window in Pager Initial Layout prefetching happens after the initial measure pass and
+     * latestPageSizeWithSpacing is updated before the prefetching happens.
+     *
+     * For scroll backed prefetching we will use the last known latestPageSizeWithSpacing.
+     */
+    private val pagerCacheWindow =
+        object : LazyLayoutCacheWindow {
+            override fun Density.calculateAheadWindow(viewport: Int): Int =
+                latestPageSizeWithSpacing
+
+            override fun Density.calculateBehindWindow(viewport: Int): Int =
+                latestPageSizeWithSpacing
+        }
+
+    internal val cacheWindowLogic =
+        PagerCacheWindowLogic(pagerCacheWindow, prefetchState) { pageCount }
 
     internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo()
 
@@ -689,6 +709,9 @@
         // should use.
         prefetchState.idealNestedPrefetchCount = result.visiblePagesInfo.size
 
+        // Update non state backed page size info
+        latestPageSizeWithSpacing = result.pageSize + result.pageSpacing
+
         if (!isLookingAhead && hasLookaheadOccurred) {
             debugLog { "Applying Approach Measure Result" }
             // If there was already a lookahead pass, record this result as Approach result
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AdaptStrategy.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AdaptStrategy.kt
index ac21947..06fdcac 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AdaptStrategy.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/AdaptStrategy.kt
@@ -91,7 +91,9 @@
      * @param scrim the scrim to show when the pane is levitated to block user interaction with the
      *   underlying layout and emphasize the levitated pane; by default it will be `null` and no
      *   scrim will show.
-     * @sample androidx.compose.material3.adaptive.samples.levitateAdaptStrategySample
+     * @sample androidx.compose.material3.adaptive.samples.levitateAsBottomSheetSample
+     * @sample androidx.compose.material3.adaptive.samples.levitateAsDialogSample
+     * @sample androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSampleWithExtraPaneLevitatedAsDialog
      * @sample androidx.compose.material3.adaptive.samples.SupportingPaneScaffoldSampleWithExtraPaneLevitatedAsBottomSheet
      * @see [onlyIf] and [onlyIfSinglePane] for finer control over when the pane should be
      *   levitated.
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Scrim.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Scrim.kt
index 2952f38..e2d7917 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Scrim.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/Scrim.kt
@@ -30,7 +30,7 @@
  * The class is used to create a scrim when a levitated pane is shown, to block the user interaction
  * from the underlying layout. See [AdaptStrategy.Levitate] for more detailed info.
  *
- * @sample androidx.compose.material3.adaptive.samples.levitateAdaptStrategySample
+ * @sample androidx.compose.material3.adaptive.samples.levitateAsDialogSample
  * @param color the color of scrim, by default if [Color.Unspecified] is provided, the pane scaffold
  *   implementation will use a translucent black color.
  * @param onClick the on-click listener of the scrim; usually used to dismiss the levitated pane;
diff --git a/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt b/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
index 89c7648..fcdb730 100644
--- a/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
+++ b/compose/material3/adaptive/samples/src/main/java/androidx/compose/material3/adaptive/samples/ThreePaneScaffoldSample.kt
@@ -40,6 +40,7 @@
 import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.selection.selectable
+import androidx.compose.foundation.selection.selectableGroup
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -99,7 +100,6 @@
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.selected
-import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
@@ -204,6 +204,61 @@
 @Preview
 @Sampled
 @Composable
+fun ListDetailPaneScaffoldSampleWithExtraPaneLevitatedAsDialog() {
+    val coroutineScope = rememberCoroutineScope()
+    val scaffoldNavigator = levitateAsDialogSample<NavItemData>()
+    val items = listOf("Item 1", "Item 2", "Item 3")
+    val extraItems = listOf("Extra 1", "Extra 2", "Extra 3")
+    val selectedItem = scaffoldNavigator.currentDestination?.contentKey
+
+    ListDetailPaneScaffold(
+        directive = scaffoldNavigator.scaffoldDirective,
+        scaffoldState = scaffoldNavigator.scaffoldState,
+        listPane = {
+            AnimatedPane(modifier = Modifier.preferredWidth(200.dp)) {
+                ListPaneContent(
+                    items = items,
+                    selectedItem = selectedItem,
+                    scaffoldNavigator = scaffoldNavigator,
+                    coroutineScope = coroutineScope,
+                )
+            }
+        },
+        detailPane = {
+            AnimatedPane {
+                DetailPaneContent(
+                    items = items,
+                    selectedItem = selectedItem,
+                    scaffoldNavigator = scaffoldNavigator,
+                    hasExtraPane = true,
+                    coroutineScope = coroutineScope,
+                )
+            }
+        },
+        extraPane = {
+            AnimatedPane {
+                ExtraPaneContent(
+                    extraItems = extraItems,
+                    selectedItem = selectedItem,
+                    scaffoldNavigator = scaffoldNavigator,
+                    coroutineScope = coroutineScope,
+                )
+            }
+        },
+        paneExpansionState =
+            rememberPaneExpansionState(
+                keyProvider = scaffoldNavigator.scaffoldValue,
+                anchors = PaneExpansionAnchors,
+                initialAnchoredIndex = 1,
+            ),
+        paneExpansionDragHandle = { state -> PaneExpansionDragHandleSample(state) },
+    )
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@Preview
+@Sampled
+@Composable
 fun SupportingPaneScaffoldSample() {
     val coroutineScope = rememberCoroutineScope()
     val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator<NavItemData>()
@@ -254,7 +309,7 @@
  * 2. The use of [androidx.compose.material3.adaptive.layout.PaneScaffoldScope.dragToResize] with
  *    [DockedEdge.Bottom] so that the levitated extra pane can be resized by dragging.
  *
- * @see levitateAdaptStrategySample for more usage samples of [AdaptStrategy.Levitate].
+ * @see levitateAsDialogSample for more usage samples of [AdaptStrategy.Levitate].
  */
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 @Preview
@@ -262,17 +317,7 @@
 @Composable
 fun SupportingPaneScaffoldSampleWithExtraPaneLevitatedAsBottomSheet() {
     val coroutineScope = rememberCoroutineScope()
-    val scaffoldDirective = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo())
-    val scaffoldNavigator =
-        rememberSupportingPaneScaffoldNavigator<NavItemData>(
-            scaffoldDirective = scaffoldDirective,
-            adaptStrategies =
-                SupportingPaneScaffoldDefaults.adaptStrategies(
-                    extraPaneAdaptStrategy =
-                        AdaptStrategy.Levitate(alignment = Alignment.BottomCenter)
-                            .onlyIfSinglePane(scaffoldDirective)
-                ),
-        )
+    val scaffoldNavigator = levitateAsBottomSheetSample<NavItemData>()
     val extraItems = listOf("Extra content")
     val selectedItem = NavItemData(index = 0, showExtra = true)
 
@@ -385,14 +430,14 @@
 
 /**
  * This sample shows how to create a [ThreePaneScaffoldNavigator] that will show the extra pane as a
- * modal dialog in a single pane layout when the extra pane is the current destination. The dialog
- * will be centered in the scaffold, with a scrim that clicking on it will dismiss the dialog.
+ * modal dialog when the extra pane is the current destination. The dialog will be centered in the
+ * scaffold, with a scrim that clicking on it will dismiss the dialog.
  */
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 @Preview
 @Sampled
 @Composable
-fun <T> levitateAdaptStrategySample(): ThreePaneScaffoldNavigator<T> {
+fun <T> levitateAsDialogSample(): ThreePaneScaffoldNavigator<T> {
     val coroutineScope = rememberCoroutineScope()
     val scaffoldDirective = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo())
     var navigator: ThreePaneScaffoldNavigator<T>? = null
@@ -417,6 +462,35 @@
     return navigator
 }
 
+/**
+ * This sample shows how to create a [ThreePaneScaffoldNavigator] that will show the extra pane as a
+ * bottom sheet in a single pane layout when the extra pane is the current destination.
+ *
+ * Note that besides the navigator, you also need to apply
+ * [androidx.compose.material3.adaptive.layout.PaneScaffoldScope.dragToResize] on the extra pane to
+ * make it be resizable by dragging. See
+ * [SupportingPaneScaffoldSampleWithExtraPaneLevitatedAsBottomSheet] for more info.
+ */
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun <T> levitateAsBottomSheetSample(): ThreePaneScaffoldNavigator<T> {
+    val scaffoldDirective = calculatePaneScaffoldDirective(currentWindowAdaptiveInfo())
+    var navigator: ThreePaneScaffoldNavigator<T>? = null
+    navigator =
+        rememberSupportingPaneScaffoldNavigator<T>(
+            scaffoldDirective = scaffoldDirective,
+            adaptStrategies =
+                SupportingPaneScaffoldDefaults.adaptStrategies(
+                    extraPaneAdaptStrategy =
+                        AdaptStrategy.Levitate(alignment = Alignment.BottomCenter)
+                            .onlyIfSinglePane(scaffoldDirective)
+                ),
+        )
+    return navigator
+}
+
 @OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class)
 @Preview
 @Sampled
@@ -585,11 +659,7 @@
                     else -> MaterialTheme.colorScheme.surface
                 }
             ),
-        modifier =
-            modifier.height(80.dp).fillMaxWidth().semantics {
-                contentDescription = title
-                selected = isSelected
-            },
+        modifier = modifier.height(80.dp).fillMaxWidth().selectable(isSelected, onClick = onClick),
     ) {
         Row(
             modifier = Modifier.padding(12.dp),
@@ -623,7 +693,10 @@
     modifier: Modifier = Modifier,
     coroutineScope: CoroutineScope,
 ) {
-    Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
+    Column(
+        modifier = modifier.selectableGroup(),
+        verticalArrangement = Arrangement.spacedBy(8.dp),
+    ) {
         items.forEachIndexed { index, item ->
             ListCard(
                 title = item,
@@ -736,7 +809,10 @@
 private fun SupportingPaneContent(modifier: Modifier = Modifier) {
     val items = listOf("Item 1", "Item 2", "Item 3")
     var selectedIndex by rememberSaveable { mutableIntStateOf(-1) }
-    Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
+    Column(
+        modifier = modifier.selectableGroup(),
+        verticalArrangement = Arrangement.spacedBy(8.dp),
+    ) {
         items.forEachIndexed { index, item ->
             ListCard(
                 title = item,
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 a29658d3..04bbcbc 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
@@ -23,7 +23,10 @@
 import androidx.compose.material3.adaptive.navigationsuite.samples.NavigationSuiteScaffoldSample
 import androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSample
 import androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSampleWithExtraPane
+import androidx.compose.material3.adaptive.samples.ListDetailPaneScaffoldSampleWithExtraPaneLevitatedAsDialog
 import androidx.compose.material3.adaptive.samples.NavigableListDetailPaneScaffoldSample
+import androidx.compose.material3.adaptive.samples.SupportingPaneScaffoldSample
+import androidx.compose.material3.adaptive.samples.SupportingPaneScaffoldSampleWithExtraPaneLevitatedAsBottomSheet
 import androidx.compose.material3.catalog.library.util.AdaptiveNavigationSuiteSampleSourceUrl
 import androidx.compose.material3.catalog.library.util.AdaptiveSampleSourceUrl
 import androidx.compose.material3.catalog.library.util.SampleSourceUrl
@@ -312,6 +315,14 @@
             ListDetailPaneScaffoldSampleWithExtraPane()
         },
         Example(
+            name = "ListDetailPaneScaffoldSampleWithExtraPaneLevitatedAsDialog",
+            description = AdaptiveExampleDescription,
+            sourceUrl = AdaptiveExampleSourceUrl,
+            isExpressive = false,
+        ) {
+            ListDetailPaneScaffoldSampleWithExtraPaneLevitatedAsDialog()
+        },
+        Example(
             name = "NavigableListDetailPaneScaffoldSample",
             description = AdaptiveExampleDescription,
             sourceUrl = AdaptiveExampleSourceUrl,
@@ -319,6 +330,22 @@
         ) {
             NavigableListDetailPaneScaffoldSample()
         },
+        Example(
+            name = "SupportingPaneScaffoldSample",
+            description = AdaptiveExampleDescription,
+            sourceUrl = AdaptiveExampleSourceUrl,
+            isExpressive = false,
+        ) {
+            SupportingPaneScaffoldSample()
+        },
+        Example(
+            name = "SupportingPaneScaffoldSampleWithExtraPaneLevitatedAsBottomSheet",
+            description = AdaptiveExampleDescription,
+            sourceUrl = AdaptiveExampleSourceUrl,
+            isExpressive = false,
+        ) {
+            SupportingPaneScaffoldSampleWithExtraPaneLevitatedAsBottomSheet()
+        },
     )
 
 private const val BadgeExampleDescription = "Badge examples"
diff --git a/compose/remote/remote-frontend/src/main/java/androidx/compose/remote/frontend/layout/RemoteTextMeasure.kt b/compose/remote/remote-frontend/src/main/java/androidx/compose/remote/frontend/layout/RemoteTextMeasure.kt
index d5fb7a7..0fe7c58 100644
--- a/compose/remote/remote-frontend/src/main/java/androidx/compose/remote/frontend/layout/RemoteTextMeasure.kt
+++ b/compose/remote/remote-frontend/src/main/java/androidx/compose/remote/frontend/layout/RemoteTextMeasure.kt
@@ -39,10 +39,11 @@
 
 private val RemoteComposeWriter.painter: Painter
     get() {
-        if (this is RemoteComposeWriterAndroid) {
-            this.painter
+        if (this !is RemoteComposeWriterAndroid) {
+            throw Exception("Invalid Writer $this, painter inaccessible")
         }
-        throw (Exception("Invalid Writer, painter inaccessible"))
+
+        return this.painter
     }
 
 @Composable
diff --git a/compose/remote/remote-player-compose/build.gradle b/compose/remote/remote-player-compose/build.gradle
new file mode 100644
index 0000000..278c640
--- /dev/null
+++ b/compose/remote/remote-player-compose/build.gradle
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.SoftwareType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+    id("AndroidXComposePlugin")
+}
+
+androidx {
+    name = "Remote Compose Player Compose"
+    type = SoftwareType.SNAPSHOT_ONLY_LIBRARY_ONLY_USED_BY_KOTLIN_CONSUMERS
+    inceptionYear = "2025"
+    description = "Compose player for RemoteCompose documents"
+    doNotDocumentReason = "Not shipped externally"
+}
+
+android {
+    namespace = "androidx.compose.remote.player.compose"
+    compileSdk = 35
+    defaultConfig {
+        minSdk = 29
+    }
+}
+
+dependencies {
+    implementation(project(":compose:remote:remote-core"))
+    implementation(project(":compose:remote:remote-player-view"))
+    implementation(libs.androidx.core)
+    implementation("androidx.appcompat:appcompat:1.7.0")
+    implementation("androidx.compose.foundation:foundation:1.8.1")
+    implementation("androidx.compose.ui:ui:1.8.1")
+}
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteComposePlayer.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteComposePlayer.kt
new file mode 100644
index 0000000..4655b69
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteComposePlayer.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.remote.player.compose
+
+import androidx.compose.foundation.Canvas
+import androidx.compose.remote.core.RemoteClock.nanoTime
+import androidx.compose.remote.core.RemoteContext
+import androidx.compose.remote.core.SystemClock
+import androidx.compose.remote.player.compose.context.ComposePaintContext
+import androidx.compose.remote.player.compose.context.ComposeRemoteContext
+import androidx.compose.remote.player.view.RemoteComposeDocument
+import androidx.compose.remote.player.view.action.NamedActionHandler
+import androidx.compose.remote.player.view.action.StateUpdaterActionCallback
+import androidx.compose.remote.player.view.state.StateUpdater
+import androidx.compose.remote.player.view.state.StateUpdaterImpl
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableLongStateOf
+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.drawscope.drawIntoCanvas
+import androidx.compose.ui.input.pointer.changedToDown
+import androidx.compose.ui.input.pointer.changedToUp
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChanged
+import androidx.compose.ui.platform.LocalHapticFeedback
+import java.time.Clock
+
+/**
+ * This is a player for a [androidx.compose.remote.player.view.RemoteComposeDocument].
+ *
+ * <p>It displays the document as well as providing the integration with the Android system (e.g.
+ * passing sensor values, etc.). It also exposes player APIs that allows to control how the document
+ * is displayed as well as reacting to document events.
+ */
+@Composable
+internal fun RemoteComposePlayer(
+    document: RemoteComposeDocument,
+    modifier: Modifier = Modifier,
+    theme: Int = -1,
+    debugMode: Int = 0,
+    clock: Clock = SystemClock(),
+    onNamedAction: (name: String, value: Any?, stateUpdater: StateUpdater) -> Unit = { _, _, _ -> },
+) {
+    var start by remember(document) { mutableLongStateOf(System.nanoTime()) }
+    var lastAnimationTime by remember(document) { mutableFloatStateOf(0.1f) }
+    val haptic = LocalHapticFeedback.current
+
+    val remoteContext by
+        remember(document) {
+            val composeRemoteContext = ComposeRemoteContext(SystemClock())
+            document.initializeContext(composeRemoteContext)
+            composeRemoteContext.isAnimationEnabled = true
+            composeRemoteContext.setDebug(debugMode)
+            composeRemoteContext.theme = theme
+            composeRemoteContext.setHaptic(haptic)
+            composeRemoteContext.loadFloat(RemoteContext.ID_TOUCH_EVENT_TIME, -Float.MAX_VALUE)
+            mutableStateOf<RemoteContext>(composeRemoteContext)
+        }
+
+    val coreDocument = document.document
+    coreDocument.addActionCallback(
+        object :
+            StateUpdaterActionCallback(
+                StateUpdaterImpl(remoteContext),
+                object : NamedActionHandler {
+                    override fun execute(name: String, value: Any?, stateUpdater: StateUpdater) {
+                        onNamedAction.invoke(name, value, stateUpdater)
+                    }
+                },
+            ) {}
+    )
+
+    val dragHappened = remember { mutableStateOf(false) }
+    Canvas(
+        modifier =
+            modifier.pointerInput(remoteContext) {
+                awaitPointerEventScope {
+                    while (true) {
+                        val event = awaitPointerEvent()
+                        for (i in 0 until event.changes.size) {
+                            val change = event.changes[i]
+                            if (change.changedToDown()) {
+                                val x = change.position.x
+                                val y = change.position.y
+                                val time = remoteContext.animationTime
+                                remoteContext.loadFloat(RemoteContext.ID_TOUCH_EVENT_TIME, time)
+                                coreDocument.touchDown(remoteContext, x, y)
+                                dragHappened.value = false
+                                change.consume()
+                            }
+                            if (change.changedToUp()) {
+                                val x = change.position.x
+                                val y = change.position.y
+                                val time = remoteContext.animationTime
+                                remoteContext.loadFloat(RemoteContext.ID_TOUCH_EVENT_TIME, time)
+                                coreDocument.touchUp(remoteContext, x, y, 0f, 0f)
+                                if (!dragHappened.value) {
+                                    coreDocument.onClick(remoteContext, x, y)
+                                }
+                                change.consume()
+                            }
+                            if (change.positionChanged()) {
+                                val x = change.position.x
+                                val y = change.position.y
+                                val time = remoteContext.animationTime
+                                remoteContext.loadFloat(RemoteContext.ID_TOUCH_EVENT_TIME, time)
+                                coreDocument.touchDrag(remoteContext, x, y)
+                                dragHappened.value = true
+                                change.consume()
+                            }
+                        }
+                    }
+                }
+            }
+    ) {
+        drawIntoCanvas {
+            it.save()
+
+            val nanoStart = nanoTime(clock)
+            val animationTime: Float = (nanoStart - start) * 1E-9f
+            remoteContext.animationTime = animationTime
+            remoteContext.loadFloat(RemoteContext.ID_ANIMATION_TIME, animationTime)
+            val loopTime: Float = animationTime - lastAnimationTime
+            remoteContext.loadFloat(RemoteContext.ID_ANIMATION_DELTA_TIME, loopTime)
+            lastAnimationTime = animationTime
+            remoteContext.currentTime = clock.millis()
+
+            remoteContext.density = density
+            remoteContext.mWidth = size.width
+            remoteContext.mHeight = size.height
+
+            remoteContext.loadFloat(RemoteContext.ID_FONT_SIZE, 30f)
+
+            remoteContext.setPaintContext(
+                ComposePaintContext(remoteContext as ComposeRemoteContext, it)
+            )
+            document.paint(remoteContext, 0)
+            it.restore()
+        }
+    }
+}
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteDocumentPlayer.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteDocumentPlayer.kt
new file mode 100644
index 0000000..6e40a4f
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/RemoteDocumentPlayer.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.remote.player.compose
+
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.size
+import androidx.compose.remote.core.CoreDocument
+import androidx.compose.remote.core.operations.Theme
+import androidx.compose.remote.player.view.RemoteComposeDocument
+import androidx.compose.remote.player.view.state.StateUpdater
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+/** A player of a [CoreDocument] */
+@Composable
+public fun RemoteDocumentPlayer(
+    document: CoreDocument,
+    documentWidth: Int,
+    documentHeight: Int,
+    modifier: Modifier = Modifier,
+    debugMode: Int = 0,
+    onNamedAction: (name: String, value: Any?, stateUpdater: StateUpdater) -> Unit = { _, _, _ -> },
+) {
+    var inDarkTheme by remember { mutableStateOf(false) }
+    var playbackTheme by remember { mutableIntStateOf(Theme.UNSPECIFIED) }
+
+    val remoteDoc = remember(document) { RemoteComposeDocument(document) }
+
+    inDarkTheme =
+        when (AppCompatDelegate.getDefaultNightMode()) {
+            AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> isSystemInDarkTheme()
+            AppCompatDelegate.MODE_NIGHT_YES -> true
+            AppCompatDelegate.MODE_NIGHT_NO -> false
+            AppCompatDelegate.MODE_NIGHT_UNSPECIFIED -> isSystemInDarkTheme()
+            else -> {
+                false
+            }
+        }
+
+    playbackTheme =
+        if (inDarkTheme) {
+            Theme.DARK
+        } else {
+            Theme.LIGHT
+        }
+
+    RemoteComposePlayer(
+        document = remoteDoc,
+        modifier = modifier.size(documentWidth.dp, documentHeight.dp),
+        theme = playbackTheme,
+        debugMode = debugMode,
+        onNamedAction = onNamedAction,
+    )
+}
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposePaintChanges.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposePaintChanges.kt
new file mode 100644
index 0000000..ce3d715
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposePaintChanges.kt
@@ -0,0 +1,370 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.remote.player.compose.context
+
+import android.annotation.SuppressLint
+import android.graphics.*
+import android.graphics.Typeface.CustomFallbackBuilder
+import android.graphics.fonts.Font
+import android.graphics.fonts.FontFamily
+import android.graphics.fonts.FontStyle
+import android.graphics.fonts.FontVariationAxis
+import android.os.Build
+import android.util.Log
+import androidx.compose.remote.core.MatrixAccess
+import androidx.compose.remote.core.RemoteContext
+import androidx.compose.remote.core.operations.ShaderData
+import androidx.compose.remote.core.operations.Utils
+import androidx.compose.remote.core.operations.paint.PaintBundle
+import androidx.compose.remote.core.operations.paint.PaintChanges
+import androidx.compose.remote.player.compose.utils.remoteToBlendMode
+import androidx.compose.remote.player.compose.utils.remoteToPorterDuffMode
+import androidx.compose.remote.player.compose.utils.toPaintingStyle
+import androidx.compose.remote.player.compose.utils.toStrokeCap
+import androidx.compose.remote.player.compose.utils.toStrokeJoin
+import androidx.compose.remote.player.view.platform.AndroidRemoteContext
+import androidx.compose.ui.graphics.NativePaint
+import java.io.File
+import java.io.IOException
+import java.nio.ByteBuffer
+
+/** A [PaintChanges] implementation for [ComposePaintContext]. */
+internal class ComposePaintChanges(
+    private val remoteContext: RemoteContext,
+    private val getPaint: () -> androidx.compose.ui.graphics.Paint,
+) : PaintChanges {
+    private var fontBuilder: Font.Builder? = null
+    val tmpMatrix: Matrix = Matrix()
+    var tileModes: Array<Shader.TileMode> =
+        arrayOf(Shader.TileMode.CLAMP, Shader.TileMode.REPEAT, Shader.TileMode.MIRROR)
+
+    override fun setTextSize(size: Float) {
+        getNativePaint().textSize = size
+    }
+
+    override fun setTypeFace(fontType: Int, weight: Int, italic: Boolean) {
+        when (fontType) {
+            PaintBundle.FONT_TYPE_DEFAULT ->
+                if (weight == 400 && !italic) { // for normal case
+                    getNativePaint().setTypeface(Typeface.DEFAULT)
+                } else {
+                    getNativePaint().setTypeface(Typeface.create(Typeface.DEFAULT, weight, italic))
+                }
+            PaintBundle.FONT_TYPE_SERIF ->
+                if (weight == 400 && !italic) { // for normal case
+                    getNativePaint().setTypeface(Typeface.SERIF)
+                } else {
+                    getNativePaint().setTypeface(Typeface.create(Typeface.SERIF, weight, italic))
+                }
+            PaintBundle.FONT_TYPE_SANS_SERIF ->
+                if (weight == 400 && !italic) { //  for normal case
+                    getNativePaint().setTypeface(Typeface.SANS_SERIF)
+                } else {
+                    getNativePaint()
+                        .setTypeface(Typeface.create(Typeface.SANS_SERIF, weight, italic))
+                }
+            PaintBundle.FONT_TYPE_MONOSPACE ->
+                if (weight == 400 && !italic) { //  for normal case
+                    getNativePaint().setTypeface(Typeface.MONOSPACE)
+                } else {
+                    getNativePaint()
+                        .setTypeface(Typeface.create(Typeface.MONOSPACE, weight, italic))
+                }
+            else -> {
+                val fi = remoteContext.getObject(fontType) as RemoteContext.FontInfo?
+                var builder = fi!!.fontBuilder as Font.Builder?
+                if (builder == null) {
+                    builder = createFontBuilder(fi.mFontData, weight, italic)
+                    fi.fontBuilder = builder
+                }
+                fontBuilder = builder
+                setAxis(null)
+            }
+        }
+    }
+
+    override fun setShaderMatrix(matrixId: Float) {
+        val id = Utils.idFromNan(matrixId)
+        if (id == 0) {
+            getNativePaint().shader.setLocalMatrix(null)
+            return
+        }
+        val matAccess = remoteContext.getObject(id) as MatrixAccess?
+        tmpMatrix.setValues(MatrixAccess.to3x3(matAccess!!.get()))
+        val s: Shader = getNativePaint().shader
+        s.setLocalMatrix(tmpMatrix)
+    }
+
+    /**
+     * @param fontType String to be looked up in system
+     * @param weight the weight of the font
+     * @param italic if the font is italic
+     */
+    override fun setTypeFace(fontType: String, weight: Int, italic: Boolean) {
+        val path = getFontPath(fontType)
+        fontBuilder = Font.Builder(File(path!!))
+        fontBuilder!!.setWeight(weight)
+        fontBuilder!!.setSlant(
+            if (italic) FontStyle.FONT_SLANT_ITALIC else FontStyle.FONT_SLANT_UPRIGHT
+        )
+        setAxis(null)
+    }
+
+    private fun createFontBuilder(data: ByteArray, weight: Int, italic: Boolean): Font.Builder {
+        val buffer = ByteBuffer.allocateDirect(data.size)
+
+        // 2. Put the fontBytes into the direct buffer.
+        buffer.put(data)
+        buffer.rewind()
+        fontBuilder = Font.Builder(buffer)
+        fontBuilder!!.setWeight(weight)
+        fontBuilder!!.setSlant(
+            if (italic) FontStyle.FONT_SLANT_ITALIC else FontStyle.FONT_SLANT_UPRIGHT
+        )
+        setAxis(null)
+        return fontBuilder!!
+    }
+
+    private fun setAxis(axis: Array<FontVariationAxis?>?) {
+        var font: Font?
+        try {
+            if (axis != null) {
+                fontBuilder!!.setFontVariationSettings(axis)
+            }
+            font = fontBuilder!!.build()
+        } catch (e: IOException) {
+            e.printStackTrace()
+            throw RuntimeException(e)
+        }
+
+        val fontFamilyBuilder = FontFamily.Builder(font)
+        val fontFamily = fontFamilyBuilder.build()
+        val typeface = CustomFallbackBuilder(fontFamily).setSystemFallback("sans-serif").build()
+        getNativePaint().setTypeface(typeface)
+    }
+
+    private fun getFontPath(fontName: String): String? {
+        var fontName = fontName
+        val fontsDir = File(SYSTEM_FONTS_PATH)
+        if (!fontsDir.exists() || !fontsDir.isDirectory()) {
+            Log.i(TAG, "System fonts directory not found")
+            return null
+        }
+
+        val fontFiles = fontsDir.listFiles()
+        if (fontFiles == null) {
+            Log.i(TAG, "Unable to list font files")
+            return null
+        }
+        fontName = fontName.lowercase()
+        for (fontFile in fontFiles) {
+            if (fontFile.getName().lowercase().contains(fontName)) {
+                return fontFile.absolutePath
+            }
+        }
+        Log.i(TAG, "font \"$fontName\" not found")
+        return null
+    }
+
+    /**
+     * Set the font variation axes
+     *
+     * @param tags tags
+     * @param values values
+     */
+    override fun setFontVariationAxes(tags: Array<String>, values: FloatArray) {
+        val axes = arrayOfNulls<FontVariationAxis>(tags.size)
+        for (i in tags.indices) {
+            axes[i] = FontVariationAxis(tags[i], values[i])
+        }
+        setAxis(axes)
+    }
+
+    /**
+     * Set the texture shader
+     *
+     * @param bitmapId the id of the bitmap to use
+     * @param tileX The tiling mode for x to draw the bitmap in.
+     * @param tileY The tiling mode for y to draw the bitmap in.
+     * @param filterMode the filter mode to be used when sampling from this shader.
+     * @param maxAnisotropy The Anisotropy value to use for filtering. Must be greater than 0.
+     */
+    override fun setTextureShader(
+        bitmapId: Int,
+        tileX: Short,
+        tileY: Short,
+        filterMode: Short,
+        maxAnisotropy: Short,
+    ) {
+
+        // TODO implement getBitmap(bitmapId)
+        val bitmap = remoteContext.mRemoteComposeState.getFromId(bitmapId) as Bitmap ?: return
+        val bs =
+            BitmapShader(
+                bitmap,
+                Shader.TileMode.entries[tileX.toInt()],
+                Shader.TileMode.entries[tileY.toInt()],
+            )
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            if (filterMode > 0) {
+                bs.filterMode = filterMode.toInt()
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+                    bs.maxAnisotropy = filterMode.toInt()
+                }
+            }
+        }
+        getPaint().shader = bs
+        // Todo cache shader once created limit cache to 10 shaders
+    }
+
+    override fun setStrokeWidth(width: Float) {
+        getPaint().strokeWidth = width
+    }
+
+    override fun setColor(color: Int) {
+        getNativePaint().setColor(color)
+    }
+
+    override fun setStrokeCap(cap: Int) {
+        getPaint().strokeCap = Paint.Cap.entries[cap].toStrokeCap()
+    }
+
+    override fun setStyle(style: Int) {
+        getPaint().style = Paint.Style.entries[style].toPaintingStyle()
+    }
+
+    @SuppressLint("NewApi")
+    override fun setShader(shaderId: Int) {
+        // TODO this stuff should check the shader creation
+        if (shaderId == 0) {
+            getNativePaint().setShader(null)
+            return
+        }
+        val data: ShaderData? = getShaderData(shaderId)
+        if (data == null) {
+            return
+        }
+        val shader = RuntimeShader(remoteContext.getText(data.shaderTextId)!!)
+        var names = data.getUniformFloatNames()
+        for (i in names.indices) {
+            val name = names[i]
+            val `val` = data.getUniformFloats(name)
+            shader.setFloatUniform(name, `val`)
+        }
+        names = data.getUniformIntegerNames()
+        for (i in names.indices) {
+            val name = names[i]
+            val `val` = data.getUniformInts(name)
+            shader.setIntUniform(name, `val`)
+        }
+        names = data.getUniformBitmapNames()
+        for (i in names.indices) {
+            val name = names[i]
+            val `val` = data.getUniformBitmapId(name)
+            val androidContext = remoteContext as AndroidRemoteContext
+            val bitmap = androidContext.mRemoteComposeState.getFromId(`val`) as Bitmap?
+            val bitmapShader = BitmapShader(bitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
+            shader.setInputShader(name, bitmapShader)
+        }
+        getNativePaint().setShader(shader)
+    }
+
+    override fun setImageFilterQuality(quality: Int) {
+        getNativePaint().isFilterBitmap = quality == 1
+    }
+
+    override fun setBlendMode(mode: Int) {
+        getPaint().blendMode = remoteToBlendMode(mode)!!
+    }
+
+    override fun setAlpha(a: Float) {
+        getNativePaint().setAlpha((255 * a).toInt())
+    }
+
+    override fun setStrokeMiter(miter: Float) {
+        getNativePaint().strokeMiter = miter
+    }
+
+    override fun setStrokeJoin(join: Int) {
+        getPaint().strokeJoin = Paint.Join.entries[join].toStrokeJoin()
+    }
+
+    override fun setFilterBitmap(filter: Boolean) {
+        getNativePaint().isFilterBitmap = filter
+    }
+
+    override fun setAntiAlias(aa: Boolean) {
+        getPaint().isAntiAlias = aa
+    }
+
+    override fun clear(mask: Long) {
+        if ((mask and (1L shl PaintBundle.COLOR_FILTER)) != 0L) {
+            getNativePaint().setColorFilter(null)
+        }
+    }
+
+    override fun setLinearGradient(
+        colors: IntArray,
+        stops: FloatArray?,
+        startX: Float,
+        startY: Float,
+        endX: Float,
+        endY: Float,
+        tileMode: Int,
+    ) {
+        getNativePaint()
+            .setShader(
+                LinearGradient(startX, startY, endX, endY, colors, stops, tileModes[tileMode])
+            )
+    }
+
+    override fun setRadialGradient(
+        colors: IntArray,
+        stops: FloatArray?,
+        centerX: Float,
+        centerY: Float,
+        radius: Float,
+        tileMode: Int,
+    ) {
+        getNativePaint()
+            .setShader(RadialGradient(centerX, centerY, radius, colors, stops, tileModes[tileMode]))
+    }
+
+    override fun setSweepGradient(
+        colors: IntArray,
+        stops: FloatArray?,
+        centerX: Float,
+        centerY: Float,
+    ) {
+        getNativePaint().setShader(SweepGradient(centerX, centerY, colors, stops))
+    }
+
+    override fun setColorFilter(color: Int, mode: Int) {
+        val porterDuffMode = remoteToPorterDuffMode(mode)
+        getNativePaint().setColorFilter(PorterDuffColorFilter(color, porterDuffMode))
+    }
+
+    private fun getNativePaint(): NativePaint = getPaint().asFrameworkPaint()
+
+    private fun getShaderData(id: Int): ShaderData? {
+        return remoteContext.mRemoteComposeState.getFromId(id) as ShaderData?
+    }
+
+    companion object {
+        private const val TAG = "ComposePaintContext"
+        private const val SYSTEM_FONTS_PATH: String = "/system/fonts/"
+    }
+}
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposePaintContext.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposePaintContext.kt
new file mode 100644
index 0000000..0cbd3dc
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposePaintContext.kt
@@ -0,0 +1,698 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.remote.player.compose.context
+
+import android.graphics.Bitmap
+import android.graphics.Outline
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.RenderEffect
+import android.graphics.RenderNode
+import android.graphics.Shader
+import android.graphics.Typeface
+import android.os.Build
+import android.text.Layout
+import android.text.StaticLayout
+import android.text.TextPaint
+import android.text.TextUtils
+import androidx.compose.remote.core.PaintContext
+import androidx.compose.remote.core.Platform
+import androidx.compose.remote.core.operations.ClipPath
+import androidx.compose.remote.core.operations.layout.managers.TextLayout
+import androidx.compose.remote.core.operations.layout.modifiers.GraphicsLayerModifierOperation
+import androidx.compose.remote.core.operations.paint.PaintBundle
+import androidx.compose.remote.player.compose.utils.FloatsToPath
+import androidx.compose.remote.player.compose.utils.copy
+import androidx.compose.remote.player.view.platform.AndroidComputedTextLayout
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.RoundRect
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.ClipOp
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.PathFillType
+import androidx.compose.ui.graphics.PathMeasure
+import androidx.compose.ui.graphics.PathOperation
+import androidx.compose.ui.graphics.asAndroidPath
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.nativeCanvas
+import kotlin.math.atan2
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+/**
+ * A [PaintContext] implementation for [androidx.compose.remote.player.compose.RemoteComposePlayer].
+ */
+internal class ComposePaintContext(
+    remoteContext: ComposeRemoteContext,
+    private var canvas: Canvas,
+) : PaintContext(remoteContext) {
+
+    var paint = Paint()
+    var paintList: MutableList<Paint> = mutableListOf()
+    var tmpRect: Rect = Rect() // use in calculation of bounds
+    var node: RenderNode? = null
+    var mainCanvas: Canvas? = null
+    var previousCanvas: Canvas? = null
+    var canvasCache: MutableMap<Bitmap, Canvas> = mutableMapOf()
+    private var cachedFontMetrics: android.graphics.Paint.FontMetrics? = null
+    private val cachedPaintChanges =
+        ComposePaintChanges(remoteContext = remoteContext, getPaint = { this.paint })
+
+    override fun drawBitmap(
+        imageId: Int,
+        srcLeft: Int,
+        srcTop: Int,
+        srcRight: Int,
+        srcBottom: Int,
+        dstLeft: Int,
+        dstTop: Int,
+        dstRight: Int,
+        dstBottom: Int,
+        cdId: Int,
+    ) {
+        val androidContext = mContext as ComposeRemoteContext
+        if (androidContext.mRemoteComposeState.containsId(imageId)) {
+            val bitmap = androidContext.mRemoteComposeState.getFromId(imageId) as Bitmap?
+            bitmap?.let {
+                nativeCanvas()
+                    .drawBitmap(
+                        bitmap,
+                        Rect(srcLeft, srcTop, srcRight, srcBottom),
+                        Rect(dstLeft, dstTop, dstRight, dstBottom),
+                        paint.asFrameworkPaint(),
+                    )
+            }
+        }
+    }
+
+    override fun scale(scaleX: Float, scaleY: Float) {
+        canvas.scale(scaleX, scaleY)
+    }
+
+    override fun translate(translateX: Float, translateY: Float) {
+        canvas.translate(translateX, translateY)
+    }
+
+    override fun drawArc(
+        left: Float,
+        top: Float,
+        right: Float,
+        bottom: Float,
+        startAngle: Float,
+        sweepAngle: Float,
+    ) {
+        canvas.drawArc(left, top, right, bottom, startAngle, sweepAngle, false, paint)
+    }
+
+    override fun drawSector(
+        left: Float,
+        top: Float,
+        right: Float,
+        bottom: Float,
+        startAngle: Float,
+        sweepAngle: Float,
+    ) {
+        canvas.drawArc(left, top, right, bottom, startAngle, sweepAngle, true, paint)
+    }
+
+    override fun drawBitmap(id: Int, left: Float, top: Float, right: Float, bottom: Float) {
+        val androidContext = mContext as ComposeRemoteContext
+        if (androidContext.mRemoteComposeState.containsId(id)) {
+            val bitmap = androidContext.mRemoteComposeState.getFromId(id) as Bitmap?
+            val src = Rect(0, 0, bitmap!!.getWidth(), bitmap.getHeight())
+            val dst = RectF(left, top, right, bottom)
+            nativeCanvas().drawBitmap(bitmap, src, dst, paint.asFrameworkPaint())
+        }
+    }
+
+    override fun drawCircle(centerX: Float, centerY: Float, radius: Float) {
+        canvas.drawCircle(Offset(centerX, centerY), radius, paint)
+    }
+
+    override fun drawLine(x1: Float, y1: Float, x2: Float, y2: Float) {
+        canvas.drawLine(Offset(x1, y1), Offset(x2, y2), paint)
+    }
+
+    override fun drawOval(left: Float, top: Float, right: Float, bottom: Float) {
+        canvas.drawOval(left, top, right, bottom, paint)
+    }
+
+    override fun drawPath(id: Int, start: Float, end: Float) {
+        canvas.drawPath(getPath(id, start, end), paint)
+    }
+
+    override fun drawRect(left: Float, top: Float, right: Float, bottom: Float) {
+        canvas.drawRect(left, top, right, bottom, paint)
+    }
+
+    override fun savePaint() {
+        paintList.add(paint.copy())
+    }
+
+    override fun restorePaint() {
+        paint = paintList.removeAt(paintList.size - 1)
+    }
+
+    override fun replacePaint(paint: PaintBundle) {
+        this.paint.asFrameworkPaint().reset()
+        applyPaint(paint)
+    }
+
+    override fun drawRoundRect(
+        left: Float,
+        top: Float,
+        right: Float,
+        bottom: Float,
+        radiusX: Float,
+        radiusY: Float,
+    ) {
+        canvas.drawRoundRect(left, top, right, bottom, radiusX, radiusY, paint)
+    }
+
+    override fun drawTextOnPath(textId: Int, pathId: Int, hOffset: Float, vOffset: Float) {
+        nativeCanvas()
+            .drawTextOnPath(
+                getText(textId)!!,
+                getNativePath(pathId, 0f, 1f),
+                hOffset,
+                vOffset,
+                paint.asFrameworkPaint(),
+            )
+    }
+
+    override fun getTextBounds(textId: Int, start: Int, end: Int, flags: Int, bounds: FloatArray) {
+        val str = getText(textId)
+
+        val endSanitized =
+            if (end == -1 || end > str!!.length) {
+                str!!.length
+            } else end
+
+        val paint = paint.asFrameworkPaint()
+        if (cachedFontMetrics == null) {
+            cachedFontMetrics = paint.getFontMetrics()
+        }
+        paint.getFontMetrics(cachedFontMetrics)
+        paint.getTextBounds(str, start, endSanitized, tmpRect)
+        if ((flags and TEXT_MEASURE_SPACES) != 0) {
+            bounds[0] = 0f
+            bounds[2] = paint.measureText(str, start, endSanitized)
+        } else {
+            bounds[0] = tmpRect.left.toFloat()
+            if ((flags and TEXT_MEASURE_MONOSPACE_WIDTH) != 0) {
+                bounds[2] = paint.measureText(str, start, endSanitized) - tmpRect.left
+            } else {
+                bounds[2] = tmpRect.right.toFloat()
+            }
+        }
+
+        if ((flags and TEXT_MEASURE_FONT_HEIGHT) != 0) {
+            bounds[1] = cachedFontMetrics!!.ascent.roundToInt().toFloat()
+            bounds[3] = cachedFontMetrics!!.descent.roundToInt().toFloat()
+        } else {
+            bounds[1] = tmpRect.top.toFloat()
+            bounds[3] = tmpRect.bottom.toFloat()
+        }
+    }
+
+    override fun layoutComplexText(
+        textId: Int,
+        start: Int,
+        end: Int,
+        alignment: Int,
+        overflow: Int,
+        maxLines: Int,
+        maxWidth: Float,
+        flags: Int,
+    ): Platform.ComputedTextLayout? {
+        val str = getText(textId)
+        if (str == null) {
+            return null
+        }
+
+        val endSanitized =
+            if (end == -1 || end > str.length) {
+                str.length
+            } else end
+
+        val textPaint = TextPaint()
+        textPaint.set(paint.asFrameworkPaint())
+        val staticLayoutBuilder =
+            StaticLayout.Builder.obtain(str, start, endSanitized, textPaint, maxWidth.toInt())
+        when (alignment) {
+            TextLayout.TEXT_ALIGN_RIGHT,
+            TextLayout.TEXT_ALIGN_END ->
+                staticLayoutBuilder.setAlignment(Layout.Alignment.ALIGN_OPPOSITE)
+            TextLayout.TEXT_ALIGN_CENTER ->
+                staticLayoutBuilder.setAlignment(Layout.Alignment.ALIGN_CENTER)
+            else -> staticLayoutBuilder.setAlignment(Layout.Alignment.ALIGN_NORMAL)
+        }
+        when (overflow) {
+            TextLayout.OVERFLOW_ELLIPSIS ->
+                staticLayoutBuilder.setEllipsize(TextUtils.TruncateAt.END)
+            TextLayout.OVERFLOW_MIDDLE_ELLIPSIS ->
+                staticLayoutBuilder.setEllipsize(TextUtils.TruncateAt.MIDDLE)
+            TextLayout.OVERFLOW_START_ELLIPSIS ->
+                staticLayoutBuilder.setEllipsize(TextUtils.TruncateAt.START)
+            else -> {}
+        }
+        staticLayoutBuilder.setMaxLines(maxLines)
+        staticLayoutBuilder.setIncludePad(false)
+
+        val staticLayout = staticLayoutBuilder.build()
+        return AndroidComputedTextLayout(
+            staticLayout,
+            staticLayout.width.toFloat(),
+            staticLayout.height.toFloat(),
+        )
+    }
+
+    override fun drawTextRun(
+        textId: Int,
+        start: Int,
+        end: Int,
+        contextStart: Int,
+        contextEnd: Int,
+        x: Float,
+        y: Float,
+        rtl: Boolean,
+    ) {
+        var textToPaint = getText(textId)
+        if (textToPaint == null) {
+            return
+        }
+        if (end == -1) {
+            if (start != 0) {
+                textToPaint = textToPaint.substring(start)
+            }
+        } else if (end > textToPaint.length) {
+            textToPaint = textToPaint.substring(start)
+        } else {
+            textToPaint = textToPaint.substring(start, end)
+        }
+
+        nativeCanvas().drawText(textToPaint, x, y, paint.asFrameworkPaint())
+    }
+
+    override fun drawComplexText(computedTextLayout: Platform.ComputedTextLayout?) {
+        if (computedTextLayout == null) {
+            return
+        }
+        val staticLayout = (computedTextLayout as AndroidComputedTextLayout).get()
+        staticLayout.draw(nativeCanvas())
+    }
+
+    override fun drawTweenPath(
+        path1Id: Int,
+        path2Id: Int,
+        tween: Float,
+        start: Float,
+        stop: Float,
+    ) {
+        canvas.drawPath(getPath(path1Id, path2Id, tween, start, stop), paint)
+    }
+
+    override fun tweenPath(out: Int, path1: Int, path2: Int, tween: Float) {
+        val p: FloatArray = getPathArray(path1, path2, tween)
+        val androidContext = mContext as ComposeRemoteContext
+        androidContext.mRemoteComposeState.putPathData(out, p)
+    }
+
+    override fun combinePath(out: Int, path1: Int, path2: Int, operation: Byte) {
+        val p1 = getPath(path1, 0f, 1f)
+        val p2 = getPath(path2, 0f, 1f)
+        val op =
+            arrayOf(
+                PathOperation.Difference,
+                PathOperation.Intersect,
+                PathOperation.ReverseDifference,
+                PathOperation.Union,
+                PathOperation.Xor,
+            )
+        val p = Path.combine(op[operation.toInt()], p1, p2)
+
+        val androidContext = mContext as ComposeRemoteContext
+        androidContext.mRemoteComposeState.putPath(out, p)
+    }
+
+    override fun applyPaint(paintData: PaintBundle) {
+        paintData.applyPaintChange(this, cachedPaintChanges)
+    }
+
+    override fun matrixScale(scaleX: Float, scaleY: Float, centerX: Float, centerY: Float) {
+        if (centerX.isNaN()) {
+            canvas.scale(scaleX, scaleY)
+        } else {
+            nativeCanvas().scale(scaleX, scaleY, centerX, centerY)
+        }
+    }
+
+    override fun matrixTranslate(translateX: Float, translateY: Float) {
+        canvas.translate(translateX, translateY)
+    }
+
+    override fun matrixSkew(skewX: Float, skewY: Float) {
+        canvas.skew(skewX, skewY)
+    }
+
+    override fun matrixRotate(rotate: Float, pivotX: Float, pivotY: Float) {
+        if (pivotX.isNaN()) {
+            canvas.rotate(rotate)
+        } else {
+            nativeCanvas().rotate(rotate, pivotX, pivotY)
+        }
+    }
+
+    override fun matrixSave() {
+        canvas.save()
+    }
+
+    override fun matrixRestore() {
+        canvas.restore()
+    }
+
+    override fun clipRect(left: Float, top: Float, right: Float, bottom: Float) {
+        canvas.clipRect(left, top, right, bottom)
+    }
+
+    override fun clipPath(pathId: Int, regionOp: Int) {
+        val path = getPath(pathId, 0f, 1f)
+        if (regionOp == ClipPath.DIFFERENCE) {
+            canvas.clipPath(path, ClipOp.Difference)
+        } else {
+            canvas.clipPath(path, ClipOp.Intersect)
+        }
+    }
+
+    override fun roundedClipRect(
+        width: Float,
+        height: Float,
+        topStart: Float,
+        topEnd: Float,
+        bottomStart: Float,
+        bottomEnd: Float,
+    ) {
+        val roundedPath = Path()
+        val roundRect =
+            RoundRect(
+                left = 0f,
+                top = 0f,
+                right = width,
+                bottom = height,
+                topLeftCornerRadius = CornerRadius(topStart, topStart),
+                topRightCornerRadius = CornerRadius(topEnd, topEnd),
+                bottomRightCornerRadius = CornerRadius(bottomEnd, bottomEnd),
+                bottomLeftCornerRadius = CornerRadius(bottomStart, bottomStart),
+            )
+        roundedPath.addRoundRect(roundRect)
+        canvas.clipPath(roundedPath)
+    }
+
+    override fun reset() {
+        with(paint.asFrameworkPaint()) {
+            // With out calling setTypeface before or after paint is reset()
+            // Variable type fonts corrupt memory resulting in a
+            // segmentation violation
+            setTypeface(Typeface.DEFAULT)
+            reset()
+        }
+    }
+
+    override fun startGraphicsLayer(w: Int, h: Int) {
+        val newNode = RenderNode("layer")
+        newNode.setPosition(0, 0, w, h)
+        node = newNode
+        previousCanvas = canvas
+        canvas = Canvas(newNode.beginRecording())
+    }
+
+    override fun setGraphicsLayer(attributes: HashMap<Int?, in Any>) {
+        node?.let {
+            val node = it
+            var hasBlurEffect = false
+            var hasOutline = false
+            for (key in attributes.keys) {
+                val value = attributes.get(key)
+                when (key) {
+                    GraphicsLayerModifierOperation.SCALE_X -> node.scaleX = value as Float
+                    GraphicsLayerModifierOperation.SCALE_Y -> node.scaleY = value as Float
+                    GraphicsLayerModifierOperation.ROTATION_X -> node.rotationX = value as Float
+                    GraphicsLayerModifierOperation.ROTATION_Y -> node.rotationY = value as Float
+                    GraphicsLayerModifierOperation.ROTATION_Z -> node.rotationZ = value as Float
+                    GraphicsLayerModifierOperation.TRANSFORM_ORIGIN_X ->
+                        node.pivotX = value as Float * node.width
+                    GraphicsLayerModifierOperation.TRANSFORM_ORIGIN_Y ->
+                        node.pivotY = value as Float * node.width
+                    GraphicsLayerModifierOperation.TRANSLATION_X ->
+                        node.translationX = value as Float
+                    GraphicsLayerModifierOperation.TRANSLATION_Y ->
+                        node.translationY = value as Float
+                    GraphicsLayerModifierOperation.TRANSLATION_Z ->
+                        node.translationZ = value as Float
+                    GraphicsLayerModifierOperation.SHAPE -> hasOutline = true
+                    GraphicsLayerModifierOperation.SHADOW_ELEVATION ->
+                        node.elevation = value as Float
+                    GraphicsLayerModifierOperation.ALPHA -> node.alpha = value as Float
+                    GraphicsLayerModifierOperation.CAMERA_DISTANCE ->
+                        node.setCameraDistance(value as Float)
+                    GraphicsLayerModifierOperation.SPOT_SHADOW_COLOR ->
+                        node.spotShadowColor = value as Int
+                    GraphicsLayerModifierOperation.AMBIENT_SHADOW_COLOR ->
+                        node.ambientShadowColor = value as Int
+                    GraphicsLayerModifierOperation.HAS_BLUR -> hasBlurEffect = (value as Int?) != 0
+                }
+            }
+            if (hasOutline) {
+                val outline = Outline()
+                outline.alpha = 1f
+                val oShape = attributes.get(GraphicsLayerModifierOperation.SHAPE)
+                if (oShape != null) {
+                    val oShapeRadius = attributes.get(GraphicsLayerModifierOperation.SHAPE_RADIUS)
+                    val type = oShape as Int
+                    if (type == GraphicsLayerModifierOperation.SHAPE_RECT) {
+                        outline.setRect(0, 0, node.width, node.height)
+                    } else if (type == GraphicsLayerModifierOperation.SHAPE_ROUND_RECT) {
+                        if (oShapeRadius != null) {
+                            val radius = oShapeRadius as Float
+                            outline.setRoundRect(Rect(0, 0, node.width, node.height), radius)
+                        } else {
+                            outline.setRect(0, 0, node.width, node.height)
+                        }
+                    } else if (type == GraphicsLayerModifierOperation.SHAPE_CIRCLE) {
+                        val radius: Float = min(node.width, node.height) / 2f
+                        outline.setRoundRect(Rect(0, 0, node.width, node.height), radius)
+                    }
+                }
+                node.setOutline(outline)
+            }
+            if (hasBlurEffect) {
+                val oBlurRadiusX = attributes.get(GraphicsLayerModifierOperation.BLUR_RADIUS_X)
+                var blurRadiusX = 0f
+                if (oBlurRadiusX != null) {
+                    blurRadiusX = oBlurRadiusX as Float
+                }
+                val oBlurRadiusY = attributes.get(GraphicsLayerModifierOperation.BLUR_RADIUS_Y)
+                var blurRadiusY = 0f
+                if (oBlurRadiusY != null) {
+                    blurRadiusY = oBlurRadiusY as Float
+                }
+                var blurTileMode = 0
+                val oBlurTileMode = attributes.get(GraphicsLayerModifierOperation.BLUR_TILE_MODE)
+                if (oBlurTileMode != null) {
+                    blurTileMode = oBlurTileMode as Int
+                }
+                var tileMode = Shader.TileMode.CLAMP
+                when (blurTileMode) {
+                    GraphicsLayerModifierOperation.TILE_MODE_CLAMP ->
+                        tileMode = Shader.TileMode.CLAMP
+                    GraphicsLayerModifierOperation.TILE_MODE_DECAL ->
+                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { // REMOVE IN PLATFORM
+                            tileMode = Shader.TileMode.DECAL
+                        } // REMOVE IN PLATFORM
+                    GraphicsLayerModifierOperation.TILE_MODE_MIRROR ->
+                        tileMode = Shader.TileMode.MIRROR
+                    GraphicsLayerModifierOperation.TILE_MODE_REPEATED ->
+                        tileMode = Shader.TileMode.REPEAT
+                }
+                if (
+                    Build.VERSION.SDK_INT // REMOVE IN PLATFORM
+                    >= Build.VERSION_CODES.S
+                ) { // REMOVE IN PLATFORM
+                    val effect = RenderEffect.createBlurEffect(blurRadiusX, blurRadiusY, tileMode)
+                    node.setRenderEffect(effect)
+                } // REMOVE IN PLATFORM
+            }
+        }
+    }
+
+    override fun endGraphicsLayer() {
+        node!!.endRecording()
+        previousCanvas?.let { canvas = it }
+        if (nativeCanvas().isHardwareAccelerated) {
+            canvas.enableZ()
+            nativeCanvas().drawRenderNode(node!!)
+            canvas.disableZ()
+        }
+
+        // node.discardDisplayList();
+        node = null
+    }
+
+    override fun getText(textId: Int): String? {
+        return mContext.mRemoteComposeState.getFromId(textId) as String?
+    }
+
+    override fun matrixFromPath(pathId: Int, fraction: Float, vOffset: Float, flags: Int) {
+        val path = getPath(pathId, 0f, 1f)
+        if (path.isEmpty) return
+
+        val measure = PathMeasure()
+        measure.setPath(path, false)
+
+        val matrix = Matrix()
+
+        measure.getMatrix(matrix, fraction, flags)
+        canvas.concat(matrix)
+    }
+
+    override fun drawToBitmap(bitmapId: Int, mode: Int, color: Int) {
+        if (mainCanvas == null) {
+            mainCanvas = canvas
+        }
+        if (bitmapId == 0) {
+            canvas = mainCanvas!!
+            return
+        }
+        val bitmap = mContext.mRemoteComposeState.getFromId(bitmapId)!! as Bitmap
+        if (canvasCache.containsKey(bitmap)) {
+            canvas = canvasCache[bitmap]!!
+
+            if ((mode and 1) == 0) {
+                bitmap.eraseColor(color)
+            }
+            return
+        }
+        canvas = Canvas(bitmap.asImageBitmap())
+        if ((mode and 1) == 0) {
+            bitmap.eraseColor(color)
+        }
+        canvasCache[bitmap] = canvas
+    }
+
+    private fun nativeCanvas() = canvas.nativeCanvas
+
+    private fun getPath(path1Id: Int, path2Id: Int, tween: Float, start: Float, end: Float): Path {
+        return getPath(getPathArray(path1Id, path2Id, tween), start, end)
+    }
+
+    private fun getPath(tmp: FloatArray, start: Float, end: Float): Path {
+        val path = Path()
+        FloatsToPath.genPath(path, tmp, start, end)
+        return path
+    }
+
+    private fun getPath(id: Int, start: Float, end: Float): Path {
+        val p: Path? = mContext.mRemoteComposeState.getPath(id) as Path?
+        val w: Int = mContext.mRemoteComposeState.getPathWinding(id)
+        if (p != null) {
+            return p
+        }
+        val path = Path()
+        val pathData: FloatArray? = mContext.mRemoteComposeState.getPathData(id)
+        if (pathData != null) {
+            FloatsToPath.genPath(path, pathData, start, end)
+            if (w == 1) {
+                path.fillType = PathFillType.EvenOdd
+            }
+            mContext.mRemoteComposeState.putPath(id, path)
+        }
+
+        return path
+    }
+
+    private fun getNativePath(id: Int, start: Float, end: Float): android.graphics.Path {
+        val androidContext = mContext as ComposeRemoteContext
+        val p = androidContext.mRemoteComposeState.getPath(id) as Path?
+        if (p != null) {
+            return p.asAndroidPath()
+        }
+        val path = android.graphics.Path()
+        val pathData = androidContext.mRemoteComposeState.getPathData(id)
+        if (pathData != null) {
+            androidx.compose.remote.player.view.platform.FloatsToPath.genPath(
+                path,
+                pathData,
+                start,
+                end,
+            )
+            androidContext.mRemoteComposeState.putPath(id, path)
+        }
+
+        return path
+    }
+
+    private fun getPathArray(path1Id: Int, path2Id: Int, tween: Float): FloatArray {
+        val androidContext = mContext as ComposeRemoteContext
+        if (tween == 0.0f) {
+            return androidContext.mRemoteComposeState.getPathData(path1Id)!!
+        }
+        if (tween == 1.0f) {
+            return androidContext.mRemoteComposeState.getPathData(path2Id)!!
+        }
+
+        val data1: FloatArray = androidContext.mRemoteComposeState.getPathData(path1Id)!!
+        val data2: FloatArray = androidContext.mRemoteComposeState.getPathData(path2Id)!!
+        val tmp = FloatArray(data2.size)
+        for (i in tmp.indices) {
+            if (java.lang.Float.isNaN(data1[i]) || java.lang.Float.isNaN(data2[i])) {
+                tmp[i] = data1[i]
+            } else {
+                tmp[i] = (data2[i] - data1[i]) * tween + data1[i]
+            }
+        }
+        return tmp
+    }
+
+    private fun PathMeasure.getMatrix(matrix: Matrix, fraction: Float, flags: Int) {
+        val len = this.length
+        if (len == 0f) return
+
+        val distanceOnPath = (len * fraction) % len
+
+        // Get position
+        val position = this.getPosition(distanceOnPath) // Returns Offset(x, y)
+
+        // Apply translation for the position
+        matrix.translate(position.x, position.y)
+
+        // Check if tangent/rotation is requested (similar to
+        // android.graphics.PathMeasure.TANGENT_MATRIX_FLAG)
+        // Android: PATH_MEASURE_TANGENT_MATRIX_FLAG = 2
+        if ((flags and 2) != 0) { // If the tangent flag is set
+            val tangent =
+                this.getTangent(distanceOnPath) // Returns Offset representing vector dx, dy
+            // Calculate rotation angle from the tangent vector
+            val angleRadians = atan2(tangent.y, tangent.x)
+            val angleDegrees = Math.toDegrees(angleRadians.toDouble()).toFloat()
+            // Apply rotation around the (translated) origin.
+            // Since we already translated, the rotation is effectively at the point on the path.
+            matrix.rotateZ(angleDegrees) // Rotate around Z-axis for 2D graphics
+        }
+    }
+}
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposeRemoteContext.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposeRemoteContext.kt
new file mode 100644
index 0000000..9297bf0
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/context/ComposeRemoteContext.kt
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.remote.player.compose.context
+
+import android.graphics.Bitmap
+import android.graphics.Bitmap.createBitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import androidx.compose.remote.core.RemoteContext
+import androidx.compose.remote.core.VariableSupport
+import androidx.compose.remote.core.operations.BitmapData
+import androidx.compose.remote.core.operations.FloatExpression
+import androidx.compose.remote.core.operations.ShaderData
+import androidx.compose.remote.core.operations.utilities.ArrayAccess
+import androidx.compose.remote.core.operations.utilities.DataMap
+import androidx.compose.remote.core.types.LongConstant
+import androidx.compose.ui.hapticfeedback.HapticFeedback
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import java.io.IOException
+import java.net.MalformedURLException
+import java.net.URL
+import java.time.Clock
+
+/**
+ * An implementation of [RemoteContext] for
+ * [androidx.compose.remote.player.compose.RemoteComposePlayer].
+ */
+internal class ComposeRemoteContext(clock: Clock) : RemoteContext(clock) {
+    private lateinit var haptic: HapticFeedback
+    private var varNameHashMap: HashMap<String, VarName?> = HashMap<String, VarName?>()
+
+    override fun loadPathData(instanceId: Int, winding: Int, floatPath: FloatArray) {
+        mRemoteComposeState.putPathData(instanceId, floatPath)
+        mRemoteComposeState.putPathWinding(instanceId, winding)
+    }
+
+    override fun getPathData(instanceId: Int): FloatArray? {
+        return mRemoteComposeState.getPathData(instanceId)
+    }
+
+    override fun loadVariableName(varName: String, varId: Int, varType: Int) {
+        varNameHashMap.put(varName, VarName(varName, varId, varType))
+    }
+
+    override fun loadColor(id: Int, color: Int) {
+        mRemoteComposeState.updateColor(id, color)
+    }
+
+    override fun setNamedColorOverride(colorName: String, color: Int) {
+        val id = varNameHashMap[colorName]!!.id
+        mRemoteComposeState.overrideColor(id, color)
+    }
+
+    override fun setNamedStringOverride(stringName: String, value: String) {
+        if (varNameHashMap[stringName] != null) {
+            val id = varNameHashMap[stringName]!!.id
+            overrideText(id, value)
+        }
+    }
+
+    fun clearDataOverride(id: Int) {
+        mRemoteComposeState.clearDataOverride(id)
+    }
+
+    fun overrideInt(id: Int, value: Int) {
+        mRemoteComposeState.overrideInteger(id, value)
+    }
+
+    fun clearIntegerOverride(id: Int) {
+        mRemoteComposeState.clearIntegerOverride(id)
+    }
+
+    fun clearFloatOverride(id: Int) {
+        mRemoteComposeState.clearFloatOverride(id)
+    }
+
+    fun overrideData(id: Int, value: Any?) {
+        mRemoteComposeState.overrideData(id, value!!)
+    }
+
+    override fun clearNamedStringOverride(stringName: String) {
+        if (varNameHashMap[stringName] != null) {
+            val id = varNameHashMap[stringName]!!.id
+            clearDataOverride(id)
+        }
+        varNameHashMap[stringName] = null
+    }
+
+    override fun setNamedIntegerOverride(stringName: String, value: Int) {
+        if (varNameHashMap[stringName] != null) {
+            val id = varNameHashMap[stringName]!!.id
+            overrideInt(id, value)
+        }
+    }
+
+    override fun clearNamedIntegerOverride(integerName: String) {
+        if (varNameHashMap[integerName] != null) {
+            val id = varNameHashMap[integerName]!!.id
+            clearIntegerOverride(id)
+        }
+        varNameHashMap[integerName] = null
+    }
+
+    override fun setNamedFloatOverride(floatName: String, value: Float) {
+        if (varNameHashMap[floatName] != null) {
+            val id = varNameHashMap[floatName]!!.id
+            overrideFloat(id, value)
+        }
+    }
+
+    override fun clearNamedFloatOverride(floatName: String) {
+        if (varNameHashMap[floatName] != null) {
+            val id = varNameHashMap[floatName]!!.id
+            clearFloatOverride(id)
+        }
+        varNameHashMap[floatName] = null
+    }
+
+    override fun setNamedLong(name: String, value: Long) {
+        val entry = varNameHashMap[name]
+        if (entry != null) {
+            val id = entry.id
+            val longConstant = mRemoteComposeState.getObject(id) as LongConstant?
+            longConstant!!.value = value
+        }
+    }
+
+    override fun setNamedDataOverride(dataName: String, value: Any) {
+        if (varNameHashMap[dataName] != null) {
+            val id = varNameHashMap[dataName]!!.id
+            overrideData(id, value)
+        }
+    }
+
+    override fun clearNamedDataOverride(dataName: String) {
+        if (varNameHashMap[dataName] != null) {
+            val id = varNameHashMap[dataName]!!.id
+            clearDataOverride(id)
+        }
+        varNameHashMap[dataName] = null
+    }
+
+    override fun addCollection(id: Int, collection: ArrayAccess) {
+        mRemoteComposeState.addCollection(id, collection)
+    }
+
+    override fun putDataMap(id: Int, map: DataMap) {
+        mRemoteComposeState.putDataMap(id, map)
+    }
+
+    override fun getDataMap(id: Int): DataMap? {
+        return mRemoteComposeState.getDataMap(id)
+    }
+
+    override fun runAction(id: Int, metadata: String) {
+        mDocument.performClick(this, id, metadata)
+    }
+
+    override fun runNamedAction(id: Int, value: Any?) {
+        val text = getText(id)
+        mDocument.runNamedAction(text!!, value)
+    }
+
+    override fun putObject(id: Int, value: Any) {
+        mRemoteComposeState.updateObject(id, value)
+    }
+
+    override fun getObject(id: Int): Any? {
+        return mRemoteComposeState.getObject(id)
+    }
+
+    override fun hapticEffect(type: Int) {
+        haptic.performHapticFeedback(HapticFeedbackType.LongPress)
+    }
+
+    override fun loadBitmap(
+        imageId: Int,
+        encoding: Short,
+        type: Short,
+        width: Int,
+        height: Int,
+        bitmap: ByteArray,
+    ) {
+        if (!mRemoteComposeState.containsId(imageId)) {
+            var image: Bitmap? = null
+            when (encoding) {
+                BitmapData.ENCODING_INLINE ->
+                    when (type) {
+                        BitmapData.TYPE_PNG_8888 -> {
+                            if (CHECK_DATA_SIZE) {
+                                val opts = BitmapFactory.Options()
+                                opts.inJustDecodeBounds = true // <-- do a bounds-only pass
+                                BitmapFactory.decodeByteArray(bitmap, 0, bitmap.size, opts)
+                                if (opts.outWidth > width || opts.outHeight > height) {
+                                    throw RuntimeException(
+                                        ("dimension don't match " +
+                                            opts.outWidth +
+                                            "x" +
+                                            opts.outHeight +
+                                            " vs " +
+                                            width +
+                                            "x" +
+                                            height)
+                                    )
+                                }
+                            }
+                            image = BitmapFactory.decodeByteArray(bitmap, 0, bitmap.size)
+                        }
+                        BitmapData.TYPE_PNG_ALPHA_8 -> {
+                            image = decodePreferringAlpha8(bitmap)
+
+                            // If needed convert to ALPHA_8.
+                            if (image!!.getConfig() != Bitmap.Config.ALPHA_8) {
+                                val alpha8Bitmap =
+                                    createBitmap(
+                                        image.getWidth(),
+                                        image.getHeight(),
+                                        Bitmap.Config.ALPHA_8,
+                                    )
+                                val canvas = Canvas(alpha8Bitmap)
+                                val paint = Paint()
+                                paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC)
+                                canvas.drawBitmap(image, 0f, 0f, paint)
+                                image.recycle() // Release resources
+
+                                image = alpha8Bitmap
+                            }
+                        }
+                        BitmapData.TYPE_RAW8888 -> {
+                            image = createBitmap(width, height, Bitmap.Config.ARGB_8888)
+                            val intData = IntArray(bitmap.size / 4)
+                            var i = 0
+                            while (i < intData.size) {
+                                val p = i * 4
+                                intData[i] =
+                                    ((bitmap[p].toInt() shl 24) or
+                                        (bitmap[p + 1].toInt() shl 16) or
+                                        (bitmap[p + 2].toInt() shl 8) or
+                                        bitmap[p + 3].toInt())
+                                i++
+                            }
+                            image.setPixels(intData, 0, width, 0, 0, width, height)
+                        }
+                        BitmapData.TYPE_RAW8 -> {
+                            image = createBitmap(width, height, Bitmap.Config.ARGB_8888)
+                            val bitmapData = IntArray(bitmap.size / 4)
+                            var i = 0
+                            while (i < bitmapData.size) {
+                                bitmapData[i] = 0x1010101 * bitmap[i]
+                                i++
+                            }
+                            image.setPixels(bitmapData, 0, width, 0, 0, width, height)
+                        }
+                    }
+                BitmapData.ENCODING_FILE -> image = BitmapFactory.decodeFile(String(bitmap))
+                BitmapData.ENCODING_URL ->
+                    try {
+                        image = BitmapFactory.decodeStream(URL(String(bitmap)).openStream())
+                    } catch (e: MalformedURLException) {
+                        throw RuntimeException(e)
+                    } catch (e: IOException) {
+                        throw RuntimeException(e)
+                    }
+                BitmapData.ENCODING_EMPTY ->
+                    image = createBitmap(width, height, Bitmap.Config.ARGB_8888)
+            }
+            mRemoteComposeState.cacheData(imageId, image!!)
+        }
+    }
+
+    override fun loadText(id: Int, text: String) {
+        if (!mRemoteComposeState.containsId(id)) {
+            mRemoteComposeState.cacheData(id, text)
+        } else {
+            mRemoteComposeState.updateData(id, text)
+        }
+    }
+
+    override fun getText(id: Int): String? {
+        return mRemoteComposeState.getFromId(id) as? String
+    }
+
+    override fun loadFloat(id: Int, value: Float) {
+        mRemoteComposeState.updateFloat(id, value)
+    }
+
+    override fun overrideFloat(id: Int, value: Float) {
+        mRemoteComposeState.overrideFloat(id, value)
+    }
+
+    override fun loadInteger(id: Int, value: Int) {
+        mRemoteComposeState.updateInteger(id, value)
+    }
+
+    override fun overrideInteger(id: Int, value: Int) {
+        mRemoteComposeState.overrideInteger(id, value)
+    }
+
+    fun overrideText(id: Int, text: String?) {
+        mRemoteComposeState.overrideData(id, text!!)
+    }
+
+    override fun overrideText(id: Int, valueId: Int) {
+        val text = getText(valueId)
+        overrideText(id, text)
+    }
+
+    override fun loadAnimatedFloat(id: Int, animatedFloat: FloatExpression) {
+        mRemoteComposeState.cacheData(id, animatedFloat)
+    }
+
+    override fun loadShader(id: Int, value: ShaderData) {
+        mRemoteComposeState.cacheData(id, value)
+    }
+
+    override fun getFloat(id: Int): Float {
+        return mRemoteComposeState.getFloat(id)
+    }
+
+    override fun getInteger(id: Int): Int {
+        return mRemoteComposeState.getInteger(id)
+    }
+
+    override fun getLong(id: Int): Long {
+        return (mRemoteComposeState.getObject(id) as LongConstant?)!!.value
+    }
+
+    override fun getColor(id: Int): Int {
+        return mRemoteComposeState.getColor(id)
+    }
+
+    override fun listensTo(id: Int, variableSupport: VariableSupport) {
+        mRemoteComposeState.listenToVar(id, variableSupport)
+    }
+
+    override fun updateOps(): Int {
+        return mRemoteComposeState.getOpsToUpdate(this, currentTime)
+    }
+
+    override fun getShader(id: Int): ShaderData? {
+        return mRemoteComposeState.getFromId(id) as ShaderData?
+    }
+
+    override fun addClickArea(
+        id: Int,
+        contentDescriptionId: Int,
+        left: Float,
+        top: Float,
+        right: Float,
+        bottom: Float,
+        metadataId: Int,
+    ) {
+        val contentDescription = mRemoteComposeState.getFromId(contentDescriptionId) as String?
+        val metadata = mRemoteComposeState.getFromId(metadataId) as String?
+        mDocument.addClickArea(id, contentDescription, left, top, right, bottom, metadata)
+    }
+
+    fun setHaptic(haptic: HapticFeedback) {
+        this@ComposeRemoteContext.haptic = haptic
+    }
+
+    private fun decodePreferringAlpha8(data: ByteArray): Bitmap? {
+        val options = BitmapFactory.Options()
+        options.inPreferredConfig = Bitmap.Config.ALPHA_8
+        return BitmapFactory.decodeByteArray(data, 0, data.size, options)
+    }
+
+    companion object {
+        private const val CHECK_DATA_SIZE: Boolean = true
+    }
+}
+
+private data class VarName(val name: String, val id: Int, val type: Int)
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/AndroidPaintUtils.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/AndroidPaintUtils.kt
new file mode 100644
index 0000000..84a887d
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/AndroidPaintUtils.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.remote.player.compose.utils
+
+import android.graphics.Paint
+import androidx.compose.ui.graphics.PaintingStyle
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.StrokeJoin
+
+/** Get a [StrokeCap] from a [Paint.Cap] */
+internal fun Paint.Cap.toStrokeCap() =
+    when (this) {
+        Paint.Cap.BUTT -> StrokeCap.Butt
+        Paint.Cap.ROUND -> StrokeCap.Round
+        Paint.Cap.SQUARE -> StrokeCap.Square
+    }
+
+/** Get a [PaintingStyle] from a [Paint.Style] */
+internal fun Paint.Style.toPaintingStyle() =
+    when (this) {
+        Paint.Style.FILL -> PaintingStyle.Fill
+        Paint.Style.STROKE -> PaintingStyle.Stroke
+        Paint.Style.FILL_AND_STROKE ->
+            // No equivalent in PaintingStyle, defaulting it to Fill
+            PaintingStyle.Fill
+    }
+
+/** Get a [StrokeJoin] from a [Paint.Join] */
+internal fun Paint.Join.toStrokeJoin() =
+    when (this) {
+        Paint.Join.MITER -> StrokeJoin.Miter
+        Paint.Join.ROUND -> StrokeJoin.Round
+        Paint.Join.BEVEL -> StrokeJoin.Bevel
+    }
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/BlendModeUtils.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/BlendModeUtils.kt
new file mode 100644
index 0000000..9f2d17a
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/BlendModeUtils.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.remote.player.compose.utils
+
+import androidx.compose.remote.core.operations.paint.PaintBundle
+import androidx.compose.ui.graphics.BlendMode
+
+/** Get a [BlendMode] from a [PaintBundle] */
+internal fun remoteToBlendMode(mode: Int): BlendMode? =
+    when (mode) {
+        PaintBundle.BLEND_MODE_CLEAR -> BlendMode.Clear
+        PaintBundle.BLEND_MODE_SRC -> BlendMode.Src
+        PaintBundle.BLEND_MODE_DST -> BlendMode.Dst
+        PaintBundle.BLEND_MODE_SRC_OVER -> BlendMode.SrcOver
+        PaintBundle.BLEND_MODE_DST_OVER -> BlendMode.DstOver
+        PaintBundle.BLEND_MODE_SRC_IN -> BlendMode.SrcIn
+        PaintBundle.BLEND_MODE_DST_IN -> BlendMode.DstIn
+        PaintBundle.BLEND_MODE_SRC_OUT -> BlendMode.SrcOut
+        PaintBundle.BLEND_MODE_DST_OUT -> BlendMode.DstOut
+        PaintBundle.BLEND_MODE_SRC_ATOP -> BlendMode.SrcAtop
+        PaintBundle.BLEND_MODE_DST_ATOP -> BlendMode.DstAtop
+        PaintBundle.BLEND_MODE_XOR -> BlendMode.Xor
+        PaintBundle.BLEND_MODE_PLUS -> BlendMode.Plus
+        PaintBundle.BLEND_MODE_MODULATE -> BlendMode.Modulate
+        PaintBundle.BLEND_MODE_SCREEN -> BlendMode.Screen
+        PaintBundle.BLEND_MODE_OVERLAY -> BlendMode.Overlay
+        PaintBundle.BLEND_MODE_DARKEN -> BlendMode.Darken
+        PaintBundle.BLEND_MODE_LIGHTEN -> BlendMode.Lighten
+        PaintBundle.BLEND_MODE_COLOR_DODGE -> BlendMode.ColorDodge
+        PaintBundle.BLEND_MODE_COLOR_BURN -> BlendMode.ColorBurn
+        PaintBundle.BLEND_MODE_HARD_LIGHT -> BlendMode.Hardlight
+        PaintBundle.BLEND_MODE_SOFT_LIGHT -> BlendMode.Softlight
+        PaintBundle.BLEND_MODE_DIFFERENCE -> BlendMode.Difference
+        PaintBundle.BLEND_MODE_EXCLUSION -> BlendMode.Exclusion
+        PaintBundle.BLEND_MODE_MULTIPLY -> BlendMode.Multiply
+        PaintBundle.BLEND_MODE_HUE -> BlendMode.Hue
+        PaintBundle.BLEND_MODE_SATURATION -> BlendMode.Saturation
+        PaintBundle.BLEND_MODE_COLOR -> BlendMode.Color
+        PaintBundle.BLEND_MODE_LUMINOSITY -> BlendMode.Luminosity
+        PaintBundle.BLEND_MODE_NULL -> null
+        else -> null
+    }
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/FloatsToPath.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/FloatsToPath.kt
new file mode 100644
index 0000000..dcba2aa
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/FloatsToPath.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.remote.player.compose.utils
+
+import android.util.Log
+import androidx.compose.remote.core.operations.PathData
+import androidx.compose.remote.core.operations.Utils.idFromNan
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.PathMeasure
+import kotlin.math.max
+import kotlin.math.min
+
+/** Utility class to convert a float array representation of a path into a Compose [Path] object. */
+internal object FloatsToPath {
+    private const val TAG = "FloatsToPath"
+
+    /**
+     * Converts a float array representing a path into a Path object.
+     *
+     * @param retPath The Path object to populate with the converted path data.
+     * @param floatPath The float array representing the path.
+     * @param start The starting percentage (0.0 to 1.0) of the path to include.
+     * @param stop The ending percentage (0.0 to 1.0) of the path to include.
+     */
+    fun genPath(retPath: Path, floatPath: FloatArray, start: Float, stop: Float) {
+        var i = 0
+        val path = Path() // todo this should be cached for performance
+        while (i < floatPath.size) {
+            when (idFromNan(floatPath[i])) {
+                PathData.MOVE -> {
+                    i++
+                    path.moveTo(floatPath[i + 0], floatPath[i + 1])
+                    i += 2
+                }
+                PathData.LINE -> {
+                    i += 3
+                    path.lineTo(floatPath[i + 0], floatPath[i + 1])
+                    i += 2
+                }
+                PathData.QUADRATIC -> {
+                    i += 3
+                    path.quadraticTo(
+                        floatPath[i + 0],
+                        floatPath[i + 1],
+                        floatPath[i + 2],
+                        floatPath[i + 3],
+                    )
+                    i += 4
+                }
+                PathData.CONIC -> {
+                    i += 3
+                    //                    if (Build.VERSION.SDK_INT >= 34) { // REMOVE IN PLATFORM
+                    //                        path.conicTo(
+                    //                            floatPath[i + 0],
+                    //                            floatPath[i + 1],
+                    //                            floatPath[i + 2],
+                    //                            floatPath[i + 3],
+                    //                            floatPath[i + 4]
+                    //                        )
+                    //                    } // REMOVE IN PLATFORM
+                    i += 5
+                    // TODO(b/434130226): Conic operation not available in
+                    // androidx.compose.ui.graphics
+                    throw UnsupportedOperationException("Conic operation not yet implemented.")
+                }
+                PathData.CUBIC -> {
+                    i += 3
+                    path.cubicTo(
+                        floatPath[i + 0],
+                        floatPath[i + 1],
+                        floatPath[i + 2],
+                        floatPath[i + 3],
+                        floatPath[i + 4],
+                        floatPath[i + 5],
+                    )
+                    i += 6
+                }
+                PathData.CLOSE -> {
+                    path.close()
+                    i++
+                }
+                PathData.DONE -> i++
+                else -> Log.w(TAG, " Odd command " + idFromNan(floatPath[i]))
+            }
+        }
+
+        retPath.reset()
+        if (start > 0f || stop < 1f) {
+            if (start < stop) {
+                val measure: PathMeasure = PathMeasure() // todo cached
+                measure.setPath(path, false)
+                val len: Float = measure.length
+                val scaleStart = (max(start.toDouble(), 0.0) * len).toFloat()
+                val scaleStop = (min(stop.toDouble(), 1.0) * len).toFloat()
+                measure.getSegment(scaleStart, scaleStop, retPath, true)
+            }
+        } else {
+            retPath.addPath(path)
+        }
+    }
+}
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/PaintUtils.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/PaintUtils.kt
new file mode 100644
index 0000000..c4b91f5
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/PaintUtils.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.remote.player.compose.utils
+
+import androidx.compose.ui.graphics.Paint
+
+/**
+ * Make a copy of [Paint]. This is not a deep copy, [Paint.shader], [Paint.colorFilter] and
+ * [Paint.pathEffect] hold the same reference as the original.
+ */
+internal fun Paint.copy(): Paint =
+    Paint().also { copy ->
+        copy.alpha = this.alpha
+        copy.isAntiAlias = this.isAntiAlias
+        copy.color = this.color
+        copy.blendMode = this.blendMode
+        copy.style = this.style
+        copy.strokeWidth = this.strokeWidth
+        copy.strokeCap = this.strokeCap
+        copy.strokeJoin = this.strokeJoin
+        copy.strokeMiterLimit = this.strokeMiterLimit
+        copy.filterQuality = this.filterQuality
+
+        // same references:
+        copy.shader = this.shader
+        copy.colorFilter = this.colorFilter
+        copy.pathEffect = this.pathEffect
+    }
diff --git a/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/PorterDuffUtils.kt b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/PorterDuffUtils.kt
new file mode 100644
index 0000000..e01378e
--- /dev/null
+++ b/compose/remote/remote-player-compose/src/main/java/androidx/compose/remote/player/compose/utils/PorterDuffUtils.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.remote.player.compose.utils
+
+import android.graphics.PorterDuff
+import androidx.compose.remote.core.operations.paint.PaintBundle
+
+/** Get a [PorterDuff.Mode] from a [PaintBundle] */
+internal fun remoteToPorterDuffMode(mode: Int): PorterDuff.Mode =
+    when (mode) {
+        PaintBundle.BLEND_MODE_CLEAR -> PorterDuff.Mode.CLEAR
+        PaintBundle.BLEND_MODE_SRC -> PorterDuff.Mode.SRC
+        PaintBundle.BLEND_MODE_DST -> PorterDuff.Mode.DST
+        PaintBundle.BLEND_MODE_SRC_OVER -> PorterDuff.Mode.SRC_OVER
+        PaintBundle.BLEND_MODE_DST_OVER -> PorterDuff.Mode.DST_OVER
+        PaintBundle.BLEND_MODE_SRC_IN -> PorterDuff.Mode.SRC_IN
+        PaintBundle.BLEND_MODE_DST_IN -> PorterDuff.Mode.DST_IN
+        PaintBundle.BLEND_MODE_SRC_OUT -> PorterDuff.Mode.SRC_OUT
+        PaintBundle.BLEND_MODE_DST_OUT -> PorterDuff.Mode.DST_OUT
+        PaintBundle.BLEND_MODE_SRC_ATOP -> PorterDuff.Mode.SRC_ATOP
+        PaintBundle.BLEND_MODE_DST_ATOP -> PorterDuff.Mode.DST_ATOP
+        PaintBundle.BLEND_MODE_XOR -> PorterDuff.Mode.XOR
+        PaintBundle.BLEND_MODE_SCREEN -> PorterDuff.Mode.SCREEN
+        PaintBundle.BLEND_MODE_OVERLAY -> PorterDuff.Mode.OVERLAY
+        PaintBundle.BLEND_MODE_DARKEN -> PorterDuff.Mode.DARKEN
+        PaintBundle.BLEND_MODE_LIGHTEN -> PorterDuff.Mode.LIGHTEN
+        PaintBundle.BLEND_MODE_MULTIPLY -> PorterDuff.Mode.MULTIPLY
+        PaintBundle.PORTER_MODE_ADD -> PorterDuff.Mode.ADD
+        else -> PorterDuff.Mode.SRC_OVER
+    }
diff --git a/compose/runtime/runtime-annotation/build.gradle b/compose/runtime/runtime-annotation/build.gradle
index 026f4bf..ec07934 100644
--- a/compose/runtime/runtime-annotation/build.gradle
+++ b/compose/runtime/runtime-annotation/build.gradle
@@ -51,24 +51,6 @@
                 api(libs.kotlinStdlib)
             }
         }
-
-        jsMain {
-            dependsOn(commonMain)
-            dependencies {
-                // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
-                // https://youtrack.jetbrains.com/issue/KT-71032
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        wasmJsMain {
-            dependsOn(commonMain)
-            dependencies {
-                // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
-                // https://youtrack.jetbrains.com/issue/KT-71032
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
     }
 }
 
diff --git a/compose/runtime/runtime-saveable/bcv/native/current.txt b/compose/runtime/runtime-saveable/bcv/native/current.txt
index e80faaa..5c1eea4 100644
--- a/compose/runtime/runtime-saveable/bcv/native/current.txt
+++ b/compose/runtime/runtime-saveable/bcv/native/current.txt
@@ -1,5 +1,5 @@
 // Klib ABI Dump
-// Targets: [linuxX64.linuxx64Stubs]
+// Targets: [iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
 // Rendering settings:
 // - Signature version: 2
 // - Show manifest properties: true
diff --git a/compose/runtime/runtime-saveable/build.gradle b/compose/runtime/runtime-saveable/build.gradle
index c674dabe..7a52f98 100644
--- a/compose/runtime/runtime-saveable/build.gradle
+++ b/compose/runtime/runtime-saveable/build.gradle
@@ -37,8 +37,15 @@
         namespace = "androidx.compose.runtime.saveable"
         androidResources.enable = true
     }
-    jvmStubs()
-    linuxX64Stubs()
+    desktop()
+    mingwX64()
+    linux()
+    mac()
+    ios()
+    tvos()
+    watchos()
+    js()
+    wasmJs()
 
     defaultPlatform(PlatformIdentifier.ANDROID)
 
@@ -46,35 +53,27 @@
         commonMain {
             dependencies {
                 implementation(libs.kotlinStdlib)
-                implementation("androidx.collection:collection:1.4.2")
-                implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
+                implementation("androidx.collection:collection:1.5.0")
+                implementation(project(":lifecycle:lifecycle-runtime-compose"))
                 api(project(":compose:runtime:runtime"))
-                api("androidx.savedstate:savedstate-compose:1.3.0")
+                api(project(":savedstate:savedstate-compose"))
             }
         }
 
         commonTest {
             dependencies {
-            }
-        }
-
-        jvmMain {
-            dependsOn(commonMain)
-            dependencies {
+                implementation(libs.kotlinTest)
+                implementation(libs.kotlinCoroutinesTest)
             }
         }
 
         androidMain {
-            dependsOn(jvmMain)
+            dependsOn(commonMain)
             dependencies {
                 api("androidx.annotation:annotation:1.8.1")
             }
         }
 
-        jvmStubsMain {
-            dependsOn(jvmMain)
-        }
-
         androidInstrumentedTest {
             dependsOn(commonTest)
             dependencies {
@@ -105,6 +104,30 @@
                 implementation(libs.truth)
             }
         }
+
+        webMain {
+            dependsOn(commonMain)
+        }
+
+        webTest {
+            dependsOn(commonTest)
+        }
+
+        jsMain {
+            dependsOn(webMain)
+        }
+
+        jsTest {
+            dependsOn(webTest)
+        }
+
+        wasmJsMain {
+            dependsOn(webMain)
+        }
+
+        wasmJsTest {
+            dependsOn(webTest)
+        }
     }
 }
 
diff --git a/compose/runtime/runtime-test-utils/build.gradle b/compose/runtime/runtime-test-utils/build.gradle
index 5cda752..44479b5 100644
--- a/compose/runtime/runtime-test-utils/build.gradle
+++ b/compose/runtime/runtime-test-utils/build.gradle
@@ -83,24 +83,6 @@
             dependsOn(commonMain)
         }
 
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
-                // https://youtrack.jetbrains.com/issue/KT-71032
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        wasmJsMain {
-            dependsOn(webMain)
-            dependencies {
-                // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
-                // https://youtrack.jetbrains.com/issue/KT-71032
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
         targets.configureEach { target ->
             if (target.platformType == KotlinPlatformType.native) {
                 if (target.konanTarget.family.appleFamily) {
@@ -110,6 +92,8 @@
                 } else {
                     target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
                 }
+            } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
+                target.compilations["main"].defaultSourceSet.dependsOn(webMain)
             }
         }
     }
diff --git a/compose/runtime/runtime/build.gradle b/compose/runtime/runtime/build.gradle
index 91e5df8..5c19a8a 100644
--- a/compose/runtime/runtime/build.gradle
+++ b/compose/runtime/runtime/build.gradle
@@ -68,9 +68,7 @@
 
         commonTest {
             dependencies {
-                // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
-                // https://youtrack.jetbrains.com/issue/KT-71032
-                implementation(libs.kotlinTestForWasmTests)
+                implementation(libs.kotlinTest)
                 implementation(libs.kotlinCoroutinesTest)
                 implementation(libs.kotlinReflect)
                 implementation(project(":compose:runtime:runtime-test-utils"))
@@ -178,39 +176,12 @@
             dependsOn(nonEmulatorCommonTest)
         }
 
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
-                // https://youtrack.jetbrains.com/issue/KT-71032
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        jsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinTestJs)
-            }
-        }
-
         wasmJsMain {
-            dependsOn(webMain)
             dependencies {
-                // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
-                // https://youtrack.jetbrains.com/issue/KT-71032
-                implementation(libs.kotlinStdlibWasm)
                 implementation(libs.kotlinXw3c)
             }
         }
 
-        wasmJsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinTestWasm)
-            }
-        }
-
         unixMain {
             dependsOn(nativeMain)
         }
@@ -247,6 +218,9 @@
                     target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
                     target.compilations["test"].defaultSourceSet.dependsOn(nativeTest)
                 }
+            } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
+                target.compilations["main"].defaultSourceSet.dependsOn(webMain)
+                target.compilations["test"].defaultSourceSet.dependsOn(webTest)
             }
         }
     }
diff --git a/compose/ui/ui-tooling-data/api/current.txt b/compose/ui/ui-tooling-data/api/current.txt
index af4a03e..94bd660 100644
--- a/compose/ui/ui-tooling-data/api/current.txt
+++ b/compose/ui/ui-tooling-data/api/current.txt
@@ -6,6 +6,10 @@
     property public java.util.List<androidx.compose.ui.tooling.data.ParameterInformation> parameters;
   }
 
+  public final class CompositionDataTreeKt {
+    method @SuppressCompatibility @androidx.compose.ui.tooling.data.UiToolingDataApi public static <T> java.util.List<T> makeTree(java.util.Set<? extends androidx.compose.runtime.tooling.CompositionData>, kotlin.jvm.functions.Function4<? super androidx.compose.runtime.tooling.CompositionGroup,? super androidx.compose.ui.tooling.data.SourceContext,? super java.util.List<? extends T>,? super java.util.List<? extends T>?,? extends T?> factory, optional androidx.compose.ui.tooling.data.ContextCache cache);
+  }
+
   @SuppressCompatibility @androidx.compose.ui.tooling.data.UiToolingDataApi public final class ContextCache {
     ctor public ContextCache();
     method public void clear();
diff --git a/compose/ui/ui-tooling-data/api/restricted_current.txt b/compose/ui/ui-tooling-data/api/restricted_current.txt
index af4a03e..94bd660 100644
--- a/compose/ui/ui-tooling-data/api/restricted_current.txt
+++ b/compose/ui/ui-tooling-data/api/restricted_current.txt
@@ -6,6 +6,10 @@
     property public java.util.List<androidx.compose.ui.tooling.data.ParameterInformation> parameters;
   }
 
+  public final class CompositionDataTreeKt {
+    method @SuppressCompatibility @androidx.compose.ui.tooling.data.UiToolingDataApi public static <T> java.util.List<T> makeTree(java.util.Set<? extends androidx.compose.runtime.tooling.CompositionData>, kotlin.jvm.functions.Function4<? super androidx.compose.runtime.tooling.CompositionGroup,? super androidx.compose.ui.tooling.data.SourceContext,? super java.util.List<? extends T>,? super java.util.List<? extends T>?,? extends T?> factory, optional androidx.compose.ui.tooling.data.ContextCache cache);
+  }
+
   @SuppressCompatibility @androidx.compose.ui.tooling.data.UiToolingDataApi public final class ContextCache {
     ctor public ContextCache();
     method public void clear();
diff --git a/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/CompositionDataTree.kt b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/CompositionDataTree.kt
new file mode 100644
index 0000000..348eacf
--- /dev/null
+++ b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/CompositionDataTree.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.data
+
+import androidx.compose.runtime.tooling.CompositionData
+import androidx.compose.runtime.tooling.CompositionGroup
+import androidx.compose.runtime.tooling.CompositionInstance
+import androidx.compose.runtime.tooling.findCompositionInstance
+
+/**
+ * Processes a set of [CompositionData] instances and constructs a list of trees of custom nodes.
+ *
+ * This function builds a hierarchy from the provided compositions and then uses the `mapTree` to
+ * transform each root composition into a custom tree structure defined by the [factory]. Results
+ * from `mapTree` that are `null` will be excluded from the final list.
+ *
+ * The processing involves:
+ * 1. Building a lookup map from [CompositionInstance] to [CompositionData].
+ * 2. Constructing a parent-to-children hierarchy of [CompositionInstance]s.
+ * 3. Recursively traversing the hierarchy to map each composition using the provided [factory],
+ *    potentially stitching children processed from sub-compositions.
+ *
+ * @param factory A function that takes a [CompositionGroup], its [SourceContext], a list of already
+ *   processed children of type [T] from the current composition, and an optional list of children
+ *   of type [T] from stitched sub-compositions. It returns a custom node of type [T] or `null`.
+ * @param cache An optional [ContextCache] to optimize [SourceContext] creation.
+ * @return A list of root nodes of type [T] representing the successfully processed trees.
+ * @receiver A set of [CompositionData] to be processed into trees.
+ */
+@UiToolingDataApi
+@OptIn(UiToolingDataApi::class)
+fun <T> Set<CompositionData>.makeTree(
+    factory: (CompositionGroup, SourceContext, List<T>, List<T>?) -> T?,
+    cache: ContextCache = ContextCache(),
+): List<T> = CompositionDataTree(this, factory, cache).build()
+
+@OptIn(UiToolingDataApi::class)
+private class CompositionDataTree<T>(
+    private val compositions: Set<CompositionData>,
+    private val factory: (CompositionGroup, SourceContext, List<T>, List<T>) -> T?,
+    private val cache: ContextCache,
+) {
+    private val hierarchy = mutableMapOf<CompositionInstance, MutableList<CompositionInstance>>()
+    private val processedNodes = mutableMapOf<CompositionInstance, T?>()
+    private val rootCompositionInstances = mutableSetOf<CompositionInstance>()
+
+    init {
+        for (compositionData in compositions) {
+            compositionData.findCompositionInstance()?.let { compositionInstance ->
+                buildCompositionParentHierarchy(compositionInstance)
+            }
+        }
+    }
+
+    fun build(): List<T> {
+        return rootCompositionInstances.mapNotNull { rootInstance -> mapTree(rootInstance) }
+    }
+
+    @OptIn(UiToolingDataApi::class)
+    private fun mapTree(instance: CompositionInstance): T? {
+        // Check if already processed to handle shared nodes and cycles
+        if (processedNodes.containsKey(instance)) {
+            return processedNodes[instance]
+        }
+        val compositionData = instance.data
+
+        // Recursively process children and collect their results
+        val children = hierarchy[instance] ?: emptyList()
+
+        for (childInstance in children) {
+            mapTree(childInstance)
+        }
+
+        val childrenToAdd = mutableMapOf<Any?, MutableList<T>>()
+        children
+            .filter { it in processedNodes }
+            .groupByTo(
+                childrenToAdd,
+                { it.findContextGroup()!!.identity },
+                { processedNodes[it]!! },
+            )
+
+        // Now, map the current tree, stitching the children's results.
+        // The `mapTreeWithStitching` function is an assumed extension that handles the actual
+        // mapping.
+        val result = compositionData.mapTreeWithStitching(factory, cache, childrenToAdd)
+
+        // Memoize the result
+        processedNodes[instance] = result
+        return result
+    }
+
+    /**
+     * Traverses up the composition tree from a given [CompositionInstance] to build its parent
+     * hierarchy and identify the ultimate root of its specific tree.
+     *
+     * Starting from the given [instance], this function iteratively moves to its parent:
+     * - It adds the current [instance] to its parent's list of children in the [hierarchy] map.
+     * - If this parent-child relationship is already recorded, it stops to avoid redundant
+     *   processing.
+     * - This continues until an instance with no parent is reached (i.e., `parentComposition` is
+     *   `null`).
+     * - The instance that has no parent is considered an ultimate root and is added to
+     *   [rootCompositionInstances].
+     *
+     * @param instance The [CompositionInstance] from which to start building the parent hierarchy.
+     */
+    private fun buildCompositionParentHierarchy(instance: CompositionInstance) {
+        var currentComposition = instance
+        var parentComposition = currentComposition.parent
+        while (parentComposition != null) {
+            val children = hierarchy.getOrPut(parentComposition) { mutableListOf() }
+            if (children.contains(currentComposition)) {
+                // This path (parent-child link) has already been processed.
+                return
+            }
+            children.add(currentComposition)
+            currentComposition = parentComposition
+            parentComposition = currentComposition.parent
+        }
+        // After the loop, currentComposition is the instance that has no parent.
+        rootCompositionInstances.add(currentComposition)
+    }
+}
diff --git a/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/SlotTree.jvm.kt b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/SlotTree.jvm.kt
index 299de92..76ef39e 100644
--- a/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/SlotTree.jvm.kt
+++ b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/SlotTree.jvm.kt
@@ -316,8 +316,9 @@
 
 @UiToolingDataApi
 private class CompositionCallStack<T>(
-    private val factory: (CompositionGroup, SourceContext, List<T>) -> T?,
+    private val factory: (CompositionGroup, SourceContext, List<T>, List<T>) -> T?,
     private val contexts: MutableMap<String, Any?>,
+    private val childrenToAdd: MutableMap<Any?, MutableList<T>>? = null,
 ) : SourceContext {
     private val stack = ArrayDeque<CompositionGroup>()
     private var currentCallIndex = 0
@@ -336,7 +337,11 @@
         box = (group.node as? LayoutInfo)?.let { boundsOfLayoutNode(it) } ?: box
         currentCallIndex = callIndex
         bounds = box
-        factory(group, this, children)?.let { out.add(it) }
+
+        val childrenToStitchToGroup =
+            childrenToAdd?.takeIf { it.isNotEmpty() }?.remove(group.identity)
+
+        factory(group, this, children, childrenToStitchToGroup ?: emptyList())?.let { out.add(it) }
         pop()
         return box
     }
@@ -456,7 +461,47 @@
     cache: ContextCache = ContextCache(),
 ): T? {
     val group = compositionGroups.firstOrNull() ?: return null
-    val callStack = CompositionCallStack(factory, cache.contexts)
+
+    // After
+    val callStack =
+        CompositionCallStack(
+            { group, sourceContext, children, _ -> factory(group, sourceContext, children) },
+            cache.contexts,
+        )
+
+    val out = mutableListOf<T>()
+    callStack.convert(group, 0, out)
+    return out.firstOrNull()
+}
+
+/**
+ * Transforms a [CompositionData] instance into a tree of custom nodes.
+ *
+ * The [factory] method is invoked for each [CompositionGroup] within the slot tree. This allows for
+ * the creation of custom nodes based on the provided arguments. The [SourceContext] argument offers
+ * access to supplementary information encoded in [CompositionGroup.sourceInfo]. If [factory]
+ * returns `null`, the entire corresponding subtree is disregarded.
+ *
+ * An optional [cache] (of type [ContextCache]) can be supplied. This can enhance performance if
+ * [mapTree] is called multiple times and the values of [CompositionGroup.sourceInfo] are not
+ * unique.
+ *
+ * @param factory A function that takes a [CompositionGroup], its [SourceContext], and a list of
+ *   already processed children of type [T], and if present children of type [T] that should be
+ *   stitched and returns a custom node of type [T] or `null` to ignore the subtree.
+ * @param cache An optional [ContextCache] to optimize [SourceContext] creation.
+ * @param childrenToAdd A map to accumulate children that need to be stitched.
+ * @return The root of the custom node tree of type [T], or `null` if the input [CompositionData] is
+ *   empty or the root group is processed to `null` by the [factory].
+ */
+@OptIn(UiToolingDataApi::class)
+internal fun <T> CompositionData.mapTreeWithStitching(
+    factory: (CompositionGroup, SourceContext, List<T>, List<T>) -> T?,
+    cache: ContextCache = ContextCache(),
+    childrenToAdd: MutableMap<Any?, MutableList<T>>? = mutableMapOf(),
+): T? {
+    val group = compositionGroups.firstOrNull() ?: return null
+    val callStack = CompositionCallStack(factory, cache.contexts, childrenToAdd)
     val out = mutableListOf<T>()
     callStack.convert(group, 0, out)
     return out.firstOrNull()
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 7e9f007..2539bddb 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -145,6 +145,7 @@
     property public boolean isGraphicsLayerShapeSemanticsEnabled;
     property public boolean isIgnoreInvalidPrevFocusRectEnabled;
     property public boolean isIndirectTouchNavigationGestureDetectorEnabled;
+    property public boolean isInitialFocusOnFocusableAvailable;
     property public boolean isNestedScrollDispatcherNodeFixEnabled;
     property public boolean isNestedScrollInteropIntegerPropagationEnabled;
     property @Deprecated public boolean isNoPinningInFocusRestorationEnabled;
@@ -165,6 +166,7 @@
     field public static boolean isGraphicsLayerShapeSemanticsEnabled;
     field public static boolean isIgnoreInvalidPrevFocusRectEnabled;
     field public static boolean isIndirectTouchNavigationGestureDetectorEnabled;
+    field public static boolean isInitialFocusOnFocusableAvailable;
     field public static boolean isNestedScrollDispatcherNodeFixEnabled;
     field public static boolean isNestedScrollInteropIntegerPropagationEnabled;
     field @Deprecated public static boolean isNoPinningInFocusRestorationEnabled;
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 385eb15..a5868b0 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -145,6 +145,7 @@
     property public boolean isGraphicsLayerShapeSemanticsEnabled;
     property public boolean isIgnoreInvalidPrevFocusRectEnabled;
     property public boolean isIndirectTouchNavigationGestureDetectorEnabled;
+    property public boolean isInitialFocusOnFocusableAvailable;
     property public boolean isNestedScrollDispatcherNodeFixEnabled;
     property public boolean isNestedScrollInteropIntegerPropagationEnabled;
     property @Deprecated public boolean isNoPinningInFocusRestorationEnabled;
@@ -165,6 +166,7 @@
     field public static boolean isGraphicsLayerShapeSemanticsEnabled;
     field public static boolean isIgnoreInvalidPrevFocusRectEnabled;
     field public static boolean isIndirectTouchNavigationGestureDetectorEnabled;
+    field public static boolean isInitialFocusOnFocusableAvailable;
     field public static boolean isNestedScrollDispatcherNodeFixEnabled;
     field public static boolean isNestedScrollInteropIntegerPropagationEnabled;
     field @Deprecated public static boolean isNoPinningInFocusRestorationEnabled;
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
index 6fcd84a..d38f9bf 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusTestUtils.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.focus
 
+import android.app.Instrumentation
 import android.content.Context
 import android.os.Build.VERSION.SDK_INT
 import android.view.View
@@ -30,6 +31,8 @@
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.nativeKeyCode
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.MeasurePolicy
 import androidx.compose.ui.platform.testTag
@@ -107,9 +110,9 @@
  * Asserts that the elements appear in the specified order.
  *
  * Consider using this helper function instead of
- * [containsExactlyElementsIn][com.google.common.truth.IterableSubject.containsExactlyElementsIn] or
- * [containsExactly][com.google.common.truth.IterableSubject.containsExactly] as it also asserts
- * that the elements are in the specified order.
+ * [containsExactlyElementsIn][IterableSubject.containsExactlyElementsIn] or
+ * [containsExactly][IterableSubject.containsExactly] as it also asserts that the elements are in
+ * the specified order.
  */
 fun IterableSubject.isExactly(vararg expected: Any?) {
     return containsExactlyElementsIn(expected).inOrder()
@@ -129,7 +132,16 @@
     Box(modifier.then(if (tag != null) Modifier.testTag(tag) else Modifier).size(50.dp).focusable())
 }
 
+fun Instrumentation.setInTouchModeCompat(touchMode: Boolean) {
+    if (touchMode) {
+        setInTouchMode(true)
+    } else {
+        // setInTouchMode(false) is flaky, so we press a key to put the system in non-touch mode.
+        sendKeyDownUpSync(Key.Grave.nativeKeyCode)
+    }
+}
+
 // TODO(b/267253920): Add a compose test API to set/reset InputMode.
-fun android.app.Instrumentation.resetInTouchModeCompat() {
+fun Instrumentation.resetInTouchModeCompat() {
     if (SDK_INT < 33) setInTouchMode(true) else resetInTouchMode()
 }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/InitialFocusTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/InitialFocusTest.kt
new file mode 100644
index 0000000..dcd2da7
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/InitialFocusTest.kt
@@ -0,0 +1,241 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.focus
+
+import android.os.Build.VERSION.SDK_INT
+import android.view.View
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@SmallTest
+@RunWith(Parameterized::class)
+class InitialFocusTest(private val initialFocusEnabled: Boolean, private val touchMode: Boolean) {
+    @get:Rule val rule = createComposeRule()
+
+    lateinit var owner: View
+    lateinit var layoutDirection: LayoutDirection
+    var previousFlagValue: Boolean = false
+
+    @Before
+    fun setTouchMode() {
+        @OptIn(ExperimentalComposeUiApi::class)
+        previousFlagValue = ComposeUiFlags.isInitialFocusOnFocusableAvailable
+        @OptIn(ExperimentalComposeUiApi::class)
+        ComposeUiFlags.isInitialFocusOnFocusableAvailable = initialFocusEnabled
+        InstrumentationRegistry.getInstrumentation().setInTouchModeCompat(touchMode)
+    }
+
+    @After
+    fun resetTouchMode() {
+        InstrumentationRegistry.getInstrumentation().resetInTouchModeCompat()
+        @OptIn(ExperimentalComposeUiApi::class)
+        ComposeUiFlags.isInitialFocusOnFocusableAvailable = previousFlagValue
+    }
+
+    @Test
+    fun noFocusable() {
+        // Arrange.
+        rule.setTestContent { Box(Modifier.size(100.dp)) }
+
+        // Assert.
+        rule.runOnIdle { assertThat(owner.hasFocus()).isFalse() }
+    }
+
+    @Test
+    fun singleFocusedItem() {
+        // Arrange.
+        rule.setTestContent { Box(Modifier.size(100.dp).testTag("box").focusable()) }
+
+        // Assert.
+        // Since API 28, we don't assign initial focus in touch mode.
+        // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
+        if (!initialFocusEnabled || touchMode && SDK_INT >= 28) {
+            assertThat(owner.hasFocus()).isFalse()
+        } else {
+            rule.onNodeWithTag("box").assertIsFocused()
+        }
+    }
+
+    @Test
+    fun topItemIsInitiallyFocused() {
+        // Arrange.
+        rule.setTestContent {
+            Column {
+                Box(Modifier.size(100.dp).testTag("top").focusable())
+                Box(Modifier.size(100.dp).focusable())
+            }
+        }
+
+        // Assert.
+        // Since API 28, we don't assign initial focus in touch mode.
+        // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
+        if (!initialFocusEnabled || touchMode && SDK_INT >= 28) {
+            assertThat(owner.hasFocus()).isFalse()
+        } else {
+            rule.onNodeWithTag("top").assertIsFocused()
+        }
+    }
+
+    @Test
+    fun leftItemIsInitiallyFocused() {
+        // Arrange.
+        rule.setTestContent {
+            Row {
+                Box(Modifier.size(100.dp).testTag("left").focusable())
+                Box(Modifier.size(100.dp).testTag("right").focusable())
+            }
+        }
+
+        // Assert.
+        // Since API 28, we don't assign initial focus in touch mode.
+        // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
+        if (!initialFocusEnabled || touchMode && SDK_INT >= 28) {
+            assertThat(owner.hasFocus()).isFalse()
+        } else {
+            when (layoutDirection) {
+                LayoutDirection.Ltr -> rule.onNodeWithTag("left").assertIsFocused()
+                LayoutDirection.Rtl -> rule.onNodeWithTag("right").assertIsFocused()
+            }
+        }
+    }
+
+    @Test
+    fun itemFocusedOnAppearing() {
+        // Arrange.
+        var showItem by mutableStateOf(false)
+        rule.setTestContent {
+            if (showItem) {
+                Box(Modifier.size(100.dp).testTag("box").focusable())
+            }
+        }
+
+        // Act.
+        rule.runOnIdle { showItem = true }
+
+        // Assert.
+        // Since API 28, we don't assign initial focus in touch mode.
+        // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
+        if (!initialFocusEnabled || touchMode && SDK_INT >= 28) {
+            assertThat(owner.hasFocus()).isFalse()
+        } else {
+            rule.onNodeWithTag("box").assertIsFocused()
+        }
+    }
+
+    @Test
+    fun topItemFocusedOnAppearing() {
+        // Arrange.
+        var showItem by mutableStateOf(false)
+        rule.setTestContent {
+            if (showItem) {
+                Column {
+                    Box(Modifier.size(100.dp).testTag("top").focusable())
+                    Box(Modifier.size(100.dp).focusable())
+                }
+            }
+        }
+
+        // Act.
+        rule.runOnIdle { showItem = true }
+
+        // Assert.
+        // Since API 28, we don't assign initial focus in touch mode.
+        // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
+        if (!initialFocusEnabled || touchMode && SDK_INT >= 28) {
+            assertThat(owner.hasFocus()).isFalse()
+        } else {
+            rule.onNodeWithTag("top").assertIsFocused()
+        }
+    }
+
+    @Test
+    fun leftItemFocusedOnAppearing() {
+        // Arrange.
+        var showItem by mutableStateOf(false)
+        rule.setTestContent {
+            if (showItem) {
+                Row {
+                    Box(Modifier.size(100.dp).testTag("left").focusable())
+                    Box(Modifier.size(100.dp).testTag("right").focusable())
+                }
+            }
+        }
+
+        // Act.
+        rule.runOnIdle { showItem = true }
+
+        // Assert.
+        // Since API 28, we don't assign initial focus in touch mode.
+        // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
+        if (!initialFocusEnabled || touchMode && SDK_INT >= 28) {
+            assertThat(owner.hasFocus()).isFalse()
+        } else {
+            when (layoutDirection) {
+                LayoutDirection.Ltr -> rule.onNodeWithTag("left").assertIsFocused()
+                LayoutDirection.Rtl -> rule.onNodeWithTag("right").assertIsFocused()
+            }
+        }
+    }
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "initialFocusEnabled = {0}, touchMode = {1}")
+        fun initParameters() =
+            listOf(
+                arrayOf(false, false),
+                arrayOf(false, true),
+                arrayOf(true, false),
+                arrayOf(true, true),
+            )
+    }
+
+    private fun ComposeContentTestRule.setTestContent(composable: @Composable () -> Unit) {
+        setContent {
+            owner = LocalView.current
+            layoutDirection = LocalLayoutDirection.current
+            composable()
+        }
+    }
+}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
index 9cc933b..7698818 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropConnectionTest.kt
@@ -22,6 +22,7 @@
 import androidx.compose.foundation.gestures.ScrollScope
 import androidx.compose.foundation.gestures.ScrollableDefaults
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
@@ -30,7 +31,9 @@
 import androidx.compose.foundation.lazy.LazyRow
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.Composable
 import androidx.compose.testutils.WithTouchSlop
 import androidx.compose.ui.Alignment
@@ -42,6 +45,7 @@
 import androidx.compose.ui.test.assertIsDisplayed
 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.test.swipeDown
 import androidx.compose.ui.test.swipeUp
@@ -576,6 +580,19 @@
         }
     }
 
+    @Test
+    fun performScrollThroughSemantics_shouldNotHang() {
+        // arrange
+        createViewComposeActivity {
+            Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
+                repeat(200) { TestItem(it.toString()) }
+            }
+        }
+
+        rule.onNodeWithTag("199").performScrollTo()
+        rule.onNodeWithTag("199").assertIsDisplayed()
+    }
+
     private fun createViewComposeActivity(
         enableInterop: Boolean = true,
         content: @Composable () -> Unit,
@@ -627,7 +644,7 @@
 @Composable
 private fun TestItem(item: String) {
     Box(
-        modifier = Modifier.padding(16.dp).height(56.dp).fillMaxWidth().testTag(item),
+        modifier = Modifier.padding(16.dp).height(96.dp).fillMaxWidth().testTag(item),
         contentAlignment = Alignment.Center,
     ) {
         BasicText(item)
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
index 54335b9..da6fe86 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/NestedScrollInteropThreeFoldTest.kt
@@ -26,7 +26,6 @@
 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
 import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
@@ -34,6 +33,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
+import kotlin.math.absoluteValue
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -140,7 +140,10 @@
         // assert
         rule.runOnIdle {
             assertThat(allConsumingConnection.offeredFromChild).isNotEqualTo(Offset.Zero)
-            assertThat(connection.consumedDownChain.round()).isEqualTo(Offset.Zero.round())
+            assertThat(connection.consumedDownChain.x.absoluteValue)
+                .isAtMost(ScrollRoundingErrorTolerance)
+            assertThat(connection.consumedDownChain.y.absoluteValue)
+                .isAtMost(ScrollRoundingErrorTolerance)
         }
     }
 
@@ -242,3 +245,5 @@
         )
     }
 }
+
+private const val ScrollRoundingErrorTolerance = 1f
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 01965d3..3068fcb 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
@@ -485,6 +485,15 @@
             findFocus()?.calculateFocusRectRelativeTo(this)
         }
 
+    // Lets the owner know that a new focus target is available.
+    override fun focusTargetAvailable() {
+        // This signal is used to assign default focus, we don't need to report anything if some
+        // component already has focus.
+        if (focusOwner.rootState.hasFocus) return
+
+        focusableViewAvailable(this@AndroidComposeView)
+    }
+
     // TODO(b/177931787) : Consider creating a KeyInputManager like we have for FocusManager so
     //  that this common logic can be used by all owners.
     private val keyInputModifier =
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/NestedScrollInteropConnection.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/NestedScrollInteropConnection.android.kt
index cbcc468..28652c8 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/NestedScrollInteropConnection.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/NestedScrollInteropConnection.android.kt
@@ -191,18 +191,26 @@
     /**
      * Since our conversion from Float to Int may result in overflow not being reported correctly we
      * need to re-add the overflow when passing the consumption data back to compose. We will assume
-     * that the overflow was also consumed.
+     * that the overflow was also consumed if something else was consumed.
      */
     val overflowX =
         if (isNestedScrollInteropIntegerPropagationEnabled) {
-            available.x - dx.reverseAxis()
+            if (consumed[0].absoluteValue == 0) {
+                0f
+            } else {
+                available.x - dx.reverseAxis()
+            }
         } else {
             0f
         }
 
     val overflowY =
         if (isNestedScrollInteropIntegerPropagationEnabled) {
-            available.y - dy.reverseAxis()
+            if (consumed[1].absoluteValue == 0) {
+                0f
+            } else {
+                available.y - dy.reverseAxis()
+            }
         } else {
             0f
         }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
index 91f8941..e7097ec 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt
@@ -154,6 +154,9 @@
     @JvmField
     var isClearFocusOnResetEnabled: Boolean = false
 
+    /** Enable initial focus when a focusable is added to a screen with no focusable content. */
+    @Suppress("MutableBareField") @JvmField var isInitialFocusOnFocusableAvailable: Boolean = false
+
     /**
      * With this flag on, the adaptive refresh rate (ARR) feature will be enabled. A preferred frame
      * rate can be set on a Composable through frame rate modifier: [Modifier.preferredFrameRate]
@@ -183,7 +186,7 @@
      */
     @Suppress("MutableBareField")
     @JvmField
-    var isNestedScrollInteropIntegerPropagationEnabled: Boolean = false
+    var isNestedScrollInteropIntegerPropagationEnabled: Boolean = true
 
     /** This flag enables clearing focus on pointer down by default. */
     @Suppress("MutableBareField") @JvmField var isClearFocusOnPointerDownEnabled: Boolean = false
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
index a945c24..555187c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
@@ -164,6 +164,9 @@
         onFocusedItem: () -> Boolean = { false },
     ): Boolean
 
+    /** Lets the FocusOwner know that a focus target is placed. */
+    fun focusTargetAvailable()
+
     /** Schedule a FocusTarget node to be invalidated after onApplyChanges. */
     fun scheduleInvalidation(node: FocusTargetNode)
 
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
index 0dbc209..b9c1f1b 100644
--- 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
@@ -423,6 +423,10 @@
         return false
     }
 
+    override fun focusTargetAvailable() {
+        platformFocusOwner.focusTargetAvailable()
+    }
+
     override fun scheduleInvalidation(node: FocusTargetNode) {
         focusInvalidationManager.scheduleInvalidation(node)
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt
index 45bd864..62fa1ce 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTargetNode.kt
@@ -16,6 +16,8 @@
 
 package androidx.compose.ui.focus
 
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.CustomDestinationResult.Cancelled
 import androidx.compose.ui.focus.CustomDestinationResult.None
@@ -36,6 +38,7 @@
 import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
 import androidx.compose.ui.modifier.ModifierLocalModifierNode
 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+import androidx.compose.ui.node.LayoutAwareModifierNode
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.node.Nodes
 import androidx.compose.ui.node.ObserverModifierNode
@@ -55,6 +58,7 @@
     private val onDispatchEventsCompleted: ((FocusTargetNode) -> Unit)? = null,
 ) :
     CompositionLocalConsumerModifierNode,
+    LayoutAwareModifierNode,
     FocusTargetModifierNode,
     ObserverModifierNode,
     ModifierLocalModifierNode,
@@ -198,6 +202,13 @@
         committedFocusState = null
     }
 
+    override fun onPlaced(coordinates: LayoutCoordinates) {
+        @OptIn(ExperimentalComposeUiApi::class)
+        if (ComposeUiFlags.isInitialFocusOnFocusableAvailable) {
+            node.requireOwner().focusOwner.focusTargetAvailable()
+        }
+    }
+
     /**
      * Visits parent [FocusPropertiesModifierNode]s and runs
      * [FocusPropertiesModifierNode.applyFocusProperties] on each parent. This effectively collects
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/PlatformFocusOwner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/PlatformFocusOwner.kt
index 6705ad8..3cd4eb5 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/PlatformFocusOwner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/PlatformFocusOwner.kt
@@ -42,4 +42,11 @@
      * view that has focus.
      */
     fun getEmbeddedViewFocusRect(): Rect?
+
+    // TODO(b/438567275) Add a default implementation that could be used for non-android devices.
+    /**
+     * Let's the owner know that a new focus target is available. The owner can use this as a signal
+     * to run initial focus.
+     */
+    fun focusTargetAvailable() {}
 }
diff --git a/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt b/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt
index dba096d..88ce5b3 100644
--- a/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt
+++ b/core/core-ktx/src/main/java/androidx/core/os/Bundle.kt
@@ -92,9 +92,9 @@
                     if (value is IBinder) {
                         this.putBinder(key, value)
                     } else if (value is Size) {
-                        BundleApi21ImplKt.putSize(this, key, value)
+                        putSize(key, value)
                     } else if (value is SizeF) {
-                        BundleApi21ImplKt.putSizeF(this, key, value)
+                        putSizeF(key, value)
                     } else {
                         val valueType = value.javaClass.canonicalName
                         throw IllegalArgumentException(
@@ -108,10 +108,3 @@
 
 /** Returns a new empty [Bundle]. */
 public fun bundleOf(): Bundle = Bundle(0)
-
-private object BundleApi21ImplKt {
-    @JvmStatic fun putSize(bundle: Bundle, key: String, value: Size?) = bundle.putSize(key, value)
-
-    @JvmStatic
-    fun putSizeF(bundle: Bundle, key: String, value: SizeF?) = bundle.putSizeF(key, value)
-}
diff --git a/core/core-splashscreen/lint-baseline.xml b/core/core-splashscreen/lint-baseline.xml
index e8aaa94..132e0bc 100644
--- a/core/core-splashscreen/lint-baseline.xml
+++ b/core/core-splashscreen/lint-baseline.xml
@@ -19,11 +19,4 @@
             file="src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt"/>
     </issue>
 
-    <issue
-        id="ObsoleteSdkInt"
-        message="This folder configuration (`v23`) is unnecessary; `minSdkVersion` is 23. Merge all the resources in this folder into `drawable`.">
-        <location
-            file="src/main/res/drawable-v23"/>
-    </issue>
-
 </issues>
diff --git a/core/core-splashscreen/src/main/res/drawable-v23/compat_splash_screen.xml b/core/core-splashscreen/src/main/res/drawable-v23/compat_splash_screen.xml
deleted file mode 100644
index e627d39..0000000
--- a/core/core-splashscreen/src/main/res/drawable-v23/compat_splash_screen.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-<?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.
-  -->
-
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:gravity="fill">
-        <color android:color="?attr/windowSplashScreenBackground" />
-    </item>
-    <item
-        android:gravity="center"
-        android:width="@dimen/splashscreen_icon_size_with_background"
-        android:height="@dimen/splashscreen_icon_size_with_background"
-        android:drawable="@drawable/icon_background">
-    </item>
-    <item
-        android:drawable="?windowSplashScreenAnimatedIcon"
-        android:gravity="center"
-        android:width="@dimen/splashscreen_icon_size_with_background"
-        android:height="@dimen/splashscreen_icon_size_with_background" />
-
-    <!-- We mask the outer bounds of the icon like we do on Android 12 -->
-    <item
-        android:gravity="center"
-        android:width="@dimen/splashscreen_icon_mask_size_with_background"
-        android:height="@dimen/splashscreen_icon_mask_size_with_background">
-        <shape android:shape="oval">
-            <solid android:color="@android:color/transparent" />
-            <stroke
-                android:width="@dimen/splashscreen_icon_mask_stroke_with_background"
-                android:color="?windowSplashScreenBackground" />
-        </shape>
-    </item>
-</layer-list>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml b/core/core-splashscreen/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml
deleted file mode 100644
index b0818ef..0000000
--- a/core/core-splashscreen/src/main/res/drawable-v23/compat_splash_screen_no_icon_background.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?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.
-  -->
-
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:gravity="fill">
-        <color android:color="?attr/windowSplashScreenBackground" />
-    </item>
-    <item
-        android:drawable="?windowSplashScreenAnimatedIcon"
-        android:gravity="center"
-        android:width="@dimen/splashscreen_icon_size_no_background"
-        android:height="@dimen/splashscreen_icon_size_no_background" />
-
-    <!-- We mask the outer bounds of the icon like we do on Android 12 -->
-    <item
-        android:gravity="center"
-        android:width="@dimen/splashscreen_icon_mask_size_no_background"
-        android:height="@dimen/splashscreen_icon_mask_size_no_background">
-        <shape android:shape="oval">
-            <solid android:color="@android:color/transparent" />
-            <stroke
-                android:width="@dimen/splashscreen_icon_mask_stroke_no_background"
-                android:color="?windowSplashScreenBackground" />
-        </shape>
-    </item>
-</layer-list>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/res/drawable/compat_splash_screen.xml b/core/core-splashscreen/src/main/res/drawable/compat_splash_screen.xml
index 28bd4d5..e627d39 100644
--- a/core/core-splashscreen/src/main/res/drawable/compat_splash_screen.xml
+++ b/core/core-splashscreen/src/main/res/drawable/compat_splash_screen.xml
@@ -14,5 +14,32 @@
   limitations under the License.
   -->
 
-<color xmlns:android="http://schemas.android.com/apk/res/android"
-    android:color="?attr/windowSplashScreenBackground" />
\ No newline at end of file
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:gravity="fill">
+        <color android:color="?attr/windowSplashScreenBackground" />
+    </item>
+    <item
+        android:gravity="center"
+        android:width="@dimen/splashscreen_icon_size_with_background"
+        android:height="@dimen/splashscreen_icon_size_with_background"
+        android:drawable="@drawable/icon_background">
+    </item>
+    <item
+        android:drawable="?windowSplashScreenAnimatedIcon"
+        android:gravity="center"
+        android:width="@dimen/splashscreen_icon_size_with_background"
+        android:height="@dimen/splashscreen_icon_size_with_background" />
+
+    <!-- We mask the outer bounds of the icon like we do on Android 12 -->
+    <item
+        android:gravity="center"
+        android:width="@dimen/splashscreen_icon_mask_size_with_background"
+        android:height="@dimen/splashscreen_icon_mask_size_with_background">
+        <shape android:shape="oval">
+            <solid android:color="@android:color/transparent" />
+            <stroke
+                android:width="@dimen/splashscreen_icon_mask_stroke_with_background"
+                android:color="?windowSplashScreenBackground" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/core/core-splashscreen/src/main/res/drawable/compat_splash_screen_no_icon_background.xml b/core/core-splashscreen/src/main/res/drawable/compat_splash_screen_no_icon_background.xml
index 28bd4d5..b0818ef 100644
--- a/core/core-splashscreen/src/main/res/drawable/compat_splash_screen_no_icon_background.xml
+++ b/core/core-splashscreen/src/main/res/drawable/compat_splash_screen_no_icon_background.xml
@@ -14,5 +14,26 @@
   limitations under the License.
   -->
 
-<color xmlns:android="http://schemas.android.com/apk/res/android"
-    android:color="?attr/windowSplashScreenBackground" />
\ No newline at end of file
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:gravity="fill">
+        <color android:color="?attr/windowSplashScreenBackground" />
+    </item>
+    <item
+        android:drawable="?windowSplashScreenAnimatedIcon"
+        android:gravity="center"
+        android:width="@dimen/splashscreen_icon_size_no_background"
+        android:height="@dimen/splashscreen_icon_size_no_background" />
+
+    <!-- We mask the outer bounds of the icon like we do on Android 12 -->
+    <item
+        android:gravity="center"
+        android:width="@dimen/splashscreen_icon_mask_size_no_background"
+        android:height="@dimen/splashscreen_icon_mask_size_no_background">
+        <shape android:shape="oval">
+            <solid android:color="@android:color/transparent" />
+            <stroke
+                android:width="@dimen/splashscreen_icon_mask_stroke_no_background"
+                android:color="?windowSplashScreenBackground" />
+        </shape>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/core/core/lint-baseline.xml b/core/core/lint-baseline.xml
index c30ee58..cdac8ff 100644
--- a/core/core/lint-baseline.xml
+++ b/core/core/lint-baseline.xml
@@ -758,27 +758,6 @@
     </issue>
 
     <issue
-        id="ObsoleteSdkInt"
-        message="This folder configuration (`v21`) is unnecessary; `minSdkVersion` is 23. Merge all the resources in this folder into `drawable`.">
-        <location
-            file="src/main/res/drawable-v21"/>
-    </issue>
-
-    <issue
-        id="ObsoleteSdkInt"
-        message="This folder configuration (`v21`) is unnecessary; `minSdkVersion` is 23. Merge all the resources in this folder into `layout`.">
-        <location
-            file="src/main/res/layout-v21"/>
-    </issue>
-
-    <issue
-        id="ObsoleteSdkInt"
-        message="This folder configuration (`v21`) is unnecessary; `minSdkVersion` is 23. Merge all the resources in this folder into `values`.">
-        <location
-            file="src/main/res/values-v21"/>
-    </issue>
-
-    <issue
         id="KotlinPropertyAccess"
         message="The getter return type (`AccessibilityNodeInfoCompat`) and setter parameter type (`View`) getter and setter methods for property `parent` should have exactly the same type to allow be accessed as a property from Kotlin; see https://android.github.io/kotlin-guides/interop.html#property-prefixes"
         errorLine1="    public AccessibilityNodeInfoCompat getParent() {"
diff --git a/core/core/src/androidTest/res/color-v23/color_state_list_themed_attrs.xml b/core/core/src/androidTest/res/color-v23/color_state_list_themed_attrs.xml
deleted file mode 100644
index 9d0137e..0000000
--- a/core/core/src/androidTest/res/color-v23/color_state_list_themed_attrs.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 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.
--->
-
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item
-        android:color="?android:attr/colorForeground"
-        android:state_enabled="false"
-        android:alpha="0.5"/>
-    <item android:color="?android:attr/colorForeground"/>
-</selector>
diff --git a/core/core/src/androidTest/res/color/color_state_list_themed_attrs.xml b/core/core/src/androidTest/res/color/color_state_list_themed_attrs.xml
index a324c86..9d0137e 100644
--- a/core/core/src/androidTest/res/color/color_state_list_themed_attrs.xml
+++ b/core/core/src/androidTest/res/color/color_state_list_themed_attrs.xml
@@ -14,11 +14,10 @@
      limitations under the License.
 -->
 
-<selector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
     <item
         android:color="?android:attr/colorForeground"
         android:state_enabled="false"
-        app:alpha="0.5" />
-    <item android:color="?android:attr/colorForeground" />
+        android:alpha="0.5"/>
+    <item android:color="?android:attr/colorForeground"/>
 </selector>
diff --git a/core/core/src/androidTest/res/values-v21/styles.xml b/core/core/src/androidTest/res/values-v21/styles.xml
deleted file mode 100644
index 9801f42..0000000
--- a/core/core/src/androidTest/res/values-v21/styles.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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.
--->
-<resources>
-    <style name="TestActivityTheme" parent="@android:style/Theme.Material">
-        <item name="android:windowAnimationStyle">@null</item>
-    </style>
-</resources>
\ No newline at end of file
diff --git a/core/core/src/androidTest/res/values/styles.xml b/core/core/src/androidTest/res/values/styles.xml
index 02c4f70..734c7df 100644
--- a/core/core/src/androidTest/res/values/styles.xml
+++ b/core/core/src/androidTest/res/values/styles.xml
@@ -14,7 +14,7 @@
      limitations under the License.
 -->
 <resources xmlns:tools="http://schemas.android.com/tools">
-    <style name="TestActivityTheme" parent="@android:style/Theme.Holo">
+    <style name="TestActivityTheme" parent="@android:style/Theme.Material">
         <item name="android:windowAnimationStyle">@null</item>
     </style>
     <style name="TextMediumStyle" parent="@android:style/TextAppearance.Medium">
diff --git a/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java b/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
index 51c60be..0ee1e0a 100644
--- a/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/app/AlarmManagerCompat.java
@@ -61,8 +61,8 @@
     @SuppressLint("MissingPermission")
     public static void setAlarmClock(@NonNull AlarmManager alarmManager, long triggerTime,
             @NonNull PendingIntent showIntent, @NonNull PendingIntent operation) {
-        Api21Impl.setAlarmClock(alarmManager,
-                Api21Impl.createAlarmClockInfo(triggerTime, showIntent), operation);
+        AlarmManager.AlarmClockInfo info = new AlarmManager.AlarmClockInfo(triggerTime, showIntent);
+        alarmManager.setAlarmClock(info, operation);
     }
 
     /**
@@ -251,22 +251,6 @@
     private AlarmManagerCompat() {
     }
 
-    static class Api21Impl {
-        private Api21Impl() {
-            // This class is not instantiable.
-        }
-
-        static void setAlarmClock(AlarmManager alarmManager, Object info,
-                PendingIntent operation) {
-            alarmManager.setAlarmClock((AlarmManager.AlarmClockInfo) info, operation);
-        }
-
-        static AlarmManager.AlarmClockInfo createAlarmClockInfo(long triggerTime,
-                PendingIntent showIntent) {
-            return new AlarmManager.AlarmClockInfo(triggerTime, showIntent);
-        }
-    }
-
     @RequiresApi(31)
     static class Api31Impl {
         private Api31Impl() {
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 ca651d6..5b67006 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -7329,7 +7329,8 @@
                 if (parcelables != null) {
                     Action[] actions = new Action[parcelables.size()];
                     for (int i = 0; i < actions.length; i++) {
-                        actions[i] = Api20Impl.getActionCompatFromAction(parcelables, i);
+                        actions[i] = getActionCompatFromAction(
+                                (Notification.Action) parcelables.get(i));
                     }
                     Collections.addAll(mActions, (Action[]) actions);
                 }
@@ -8079,22 +8080,6 @@
 
         /**
          * A class for wrapping calls to {@link Notification.WearableExtender} methods which
-         * were added in API 20; these calls must be wrapped to avoid performance issues.
-         * See the UnsafeNewApiCall lint rule for more details.
-         */
-        static class Api20Impl {
-            private Api20Impl() { }
-
-            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));
-            }
-        }
-
-        /**
-         * A class for wrapping calls to {@link Notification.WearableExtender} methods which
          * were added in API 24; these calls must be wrapped to avoid performance issues.
          * See the UnsafeNewApiCall lint rule for more details.
          */
diff --git a/core/core/src/main/java/androidx/core/content/FileProvider.java b/core/core/src/main/java/androidx/core/content/FileProvider.java
index 133d549..7a8be8f 100644
--- a/core/core/src/main/java/androidx/core/content/FileProvider.java
+++ b/core/core/src/main/java/androidx/core/content/FileProvider.java
@@ -534,7 +534,7 @@
                         target = externalCacheDirs[0];
                     }
                 } else if (TAG_EXTERNAL_MEDIA.equals(tag)) {
-                    File[] externalMediaDirs = Api21Impl.getExternalMediaDirs(context);
+                    File[] externalMediaDirs = context.getExternalMediaDirs();
                     if (externalMediaDirs.length > 0) {
                         target = externalMediaDirs[0];
                     }
@@ -972,15 +972,4 @@
             return filePath.startsWith(rootPath + '/');
         }
     }
-
-    static class Api21Impl {
-        private Api21Impl() {
-            // This class is not instantiable.
-        }
-
-        static File[] getExternalMediaDirs(Context context) {
-            // Deprecated, otherwise this would belong on ContextCompat as a public method.
-            return context.getExternalMediaDirs();
-        }
-    }
 }
diff --git a/core/core/src/main/java/androidx/core/os/LocaleListCompat.java b/core/core/src/main/java/androidx/core/os/LocaleListCompat.java
index 0e70196..bd53921 100644
--- a/core/core/src/main/java/androidx/core/os/LocaleListCompat.java
+++ b/core/core/src/main/java/androidx/core/os/LocaleListCompat.java
@@ -156,7 +156,7 @@
             final String[] tags = list.split(",", -1);
             final Locale[] localeArray = new Locale[tags.length];
             for (int i = 0; i < localeArray.length; i++) {
-                localeArray[i] = Api21Impl.forLanguageTag(tags[i]);
+                localeArray[i] = Locale.forLanguageTag(tags[i]);
             }
             return create(localeArray);
         }
@@ -289,10 +289,6 @@
             }
             return false;
         }
-
-        static Locale forLanguageTag(String languageTag) {
-            return Locale.forLanguageTag(languageTag);
-        }
     }
 
     @Override
diff --git a/core/core/src/main/java/androidx/core/os/LocaleListCompatWrapper.java b/core/core/src/main/java/androidx/core/os/LocaleListCompatWrapper.java
index 52e44c1..5438e6b 100644
--- a/core/core/src/main/java/androidx/core/os/LocaleListCompatWrapper.java
+++ b/core/core/src/main/java/androidx/core/os/LocaleListCompatWrapper.java
@@ -156,7 +156,7 @@
     }
 
     private static String getLikelyScript(Locale locale) {
-        final String script = Api21Impl.getScript(locale);
+        final String script = locale.getScript();
         if (!script.isEmpty()) {
             return script;
         } else {
@@ -261,14 +261,4 @@
         return computeFirstMatch(Arrays.asList(supportedLocales),
                 false /* assume English is not supported */);
     }
-
-    static class Api21Impl {
-        private Api21Impl() {
-            // This class is not instantiable.
-        }
-
-        static String getScript(Locale locale) {
-            return locale.getScript();
-        }
-    }
 }
diff --git a/core/core/src/main/java/androidx/core/text/ICUCompat.java b/core/core/src/main/java/androidx/core/text/ICUCompat.java
index c1168e1..bd56e91 100644
--- a/core/core/src/main/java/androidx/core/text/ICUCompat.java
+++ b/core/core/src/main/java/androidx/core/text/ICUCompat.java
@@ -76,15 +76,16 @@
         } else {
             try {
                 final Object[] args = new Object[] { locale };
+                Locale localeWithSubtags = (Locale) sAddLikelySubtagsMethod.invoke(null, args);
                 // ULocale.addLikelySubtags(ULocale) is @NonNull
                 //noinspection ConstantConditions
-                return Api21Impl.getScript((Locale) sAddLikelySubtagsMethod.invoke(null, args));
+                return localeWithSubtags.getScript();
             } catch (InvocationTargetException e) {
                 Log.w(TAG, e);
             } catch (IllegalAccessException e) {
                 Log.w(TAG, e);
             }
-            return Api21Impl.getScript(locale);
+            return locale.getScript();
         }
     }
     private ICUCompat() {
@@ -108,14 +109,4 @@
             return ((ULocale) uLocale).getScript();
         }
     }
-
-    static class Api21Impl {
-        private Api21Impl() {
-            // This class is not instantiable.
-        }
-
-        static String getScript(Locale locale) {
-            return locale.getScript();
-        }
-    }
 }
diff --git a/core/core/src/main/java/androidx/core/util/SizeFCompat.java b/core/core/src/main/java/androidx/core/util/SizeFCompat.java
index 3259283..35decbb 100644
--- a/core/core/src/main/java/androidx/core/util/SizeFCompat.java
+++ b/core/core/src/main/java/androidx/core/util/SizeFCompat.java
@@ -72,23 +72,11 @@
 
     /** Converts this {@link SizeFCompat} into a {@link SizeF}. */
     public @NonNull SizeF toSizeF() {
-        return Api21Impl.toSizeF(this);
+        return new SizeF(getWidth(), getHeight());
     }
 
     /** Converts this {@link SizeF} into a {@link SizeFCompat}. */
     public static @NonNull SizeFCompat toSizeFCompat(@NonNull SizeF size) {
-        return Api21Impl.toSizeFCompat(size);
-    }
-
-    private static final class Api21Impl {
-        static @NonNull SizeFCompat toSizeFCompat(@NonNull SizeF size) {
-            Preconditions.checkNotNull(size);
-            return new SizeFCompat(size.getWidth(), size.getHeight());
-        }
-
-        static @NonNull SizeF toSizeF(@NonNull SizeFCompat size) {
-            Preconditions.checkNotNull(size);
-            return new SizeF(size.getWidth(), size.getHeight());
-        }
+        return new SizeFCompat(size.getWidth(), size.getHeight());
     }
 }
diff --git a/core/core/src/main/java/androidx/core/view/animation/PathInterpolatorCompat.java b/core/core/src/main/java/androidx/core/view/animation/PathInterpolatorCompat.java
index f3f9d8a..0b3ff60 100644
--- a/core/core/src/main/java/androidx/core/view/animation/PathInterpolatorCompat.java
+++ b/core/core/src/main/java/androidx/core/view/animation/PathInterpolatorCompat.java
@@ -23,9 +23,8 @@
 import org.jspecify.annotations.NonNull;
 
 /**
- * Helper for creating path-based {@link Interpolator} instances. On API 21 or newer, the
- * platform implementation will be used and on older platforms a compatible alternative
- * implementation will be used.
+ * Helper for creating path-based {@link Interpolator} instances. The platform implementation will
+ * be used.
  */
 public final class PathInterpolatorCompat {
 
@@ -46,7 +45,7 @@
      * @return the {@link Interpolator} representing the {@link Path}
      */
     public static @NonNull Interpolator create(@NonNull Path path) {
-        return Api21Impl.createPathInterpolator(path);
+        return new PathInterpolator(path);
     }
 
     /**
@@ -58,7 +57,7 @@
      * @return the {@link Interpolator} representing the quadratic Bezier curve
      */
     public static @NonNull Interpolator create(float controlX, float controlY) {
-        return Api21Impl.createPathInterpolator(controlX, controlY);
+        return new PathInterpolator(controlX, controlY);
     }
 
     /**
@@ -73,25 +72,6 @@
      */
     public static @NonNull Interpolator create(float controlX1, float controlY1,
             float controlX2, float controlY2) {
-        return Api21Impl.createPathInterpolator(controlX1, controlY1, controlX2, controlY2);
-    }
-
-    static class Api21Impl {
-        private Api21Impl() {
-            // This class is not instantiable.
-        }
-
-        static Interpolator createPathInterpolator(Path path) {
-            return new PathInterpolator(path);
-        }
-
-        static Interpolator createPathInterpolator(float controlX, float controlY) {
-            return new PathInterpolator(controlX, controlY);
-        }
-
-        static Interpolator createPathInterpolator(float controlX1, float controlY1,
-                float controlX2, float controlY2) {
-            return new PathInterpolator(controlX1, controlY1, controlX2, controlY2);
-        }
+        return new PathInterpolator(controlX1, controlY1, controlX2, controlY2);
     }
 }
diff --git a/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java b/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java
index 100c77f..3bd2dd0 100644
--- a/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/CheckedTextViewCompat.java
@@ -48,7 +48,7 @@
      */
     public static void setCheckMarkTintList(@NonNull CheckedTextView textView,
             @Nullable ColorStateList tint) {
-        Api21Impl.setCheckMarkTintList(textView, tint);
+        textView.setCheckMarkTintList(tint);
     }
 
     /**
@@ -57,7 +57,7 @@
      * @see #setCheckMarkTintList(CheckedTextView, ColorStateList)
      */
     public static @Nullable ColorStateList getCheckMarkTintList(@NonNull CheckedTextView textView) {
-        return Api21Impl.getCheckMarkTintList(textView);
+        return textView.getCheckMarkTintList();
     }
 
     /**
@@ -73,7 +73,7 @@
      */
     public static void setCheckMarkTintMode(@NonNull CheckedTextView textView,
             PorterDuff.@Nullable Mode tintMode) {
-        Api21Impl.setCheckMarkTintMode(textView, tintMode);
+        textView.setCheckMarkTintMode(tintMode);
     }
 
     /**
@@ -83,7 +83,7 @@
      */
     public static PorterDuff.@Nullable Mode getCheckMarkTintMode(
             @NonNull CheckedTextView textView) {
-        return Api21Impl.getCheckMarkTintMode(textView);
+        return textView.getCheckMarkTintMode();
     }
 
     /**
@@ -97,28 +97,4 @@
     public static @Nullable Drawable getCheckMarkDrawable(@NonNull CheckedTextView textView) {
         return textView.getCheckMarkDrawable();
     }
-
-    private static class Api21Impl {
-
-        private Api21Impl() {
-        }
-
-        static void setCheckMarkTintList(@NonNull CheckedTextView textView,
-                @Nullable ColorStateList tint) {
-            textView.setCheckMarkTintList(tint);
-        }
-
-        static @Nullable ColorStateList getCheckMarkTintList(@NonNull CheckedTextView textView) {
-            return textView.getCheckMarkTintList();
-        }
-
-        static void setCheckMarkTintMode(@NonNull CheckedTextView textView,
-                PorterDuff.@Nullable Mode tintMode) {
-            textView.setCheckMarkTintMode(tintMode);
-        }
-
-        static PorterDuff.@Nullable Mode getCheckMarkTintMode(@NonNull CheckedTextView textView) {
-            return textView.getCheckMarkTintMode();
-        }
-    }
 }
diff --git a/core/core/src/main/java/androidx/core/widget/CompoundButtonCompat.java b/core/core/src/main/java/androidx/core/widget/CompoundButtonCompat.java
index 89cc49f..2376edb 100644
--- a/core/core/src/main/java/androidx/core/widget/CompoundButtonCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/CompoundButtonCompat.java
@@ -47,7 +47,7 @@
      */
     public static void setButtonTintList(@NonNull CompoundButton button,
             @Nullable ColorStateList tint) {
-        Api21Impl.setButtonTintList(button, tint);
+        button.setButtonTintList(tint);
     }
 
     /**
@@ -56,7 +56,7 @@
      * @see #setButtonTintList(CompoundButton, ColorStateList)
      */
     public static @Nullable ColorStateList getButtonTintList(@NonNull CompoundButton button) {
-        return Api21Impl.getButtonTintList(button);
+        return button.getButtonTintList();
     }
 
     /**
@@ -73,7 +73,7 @@
      */
     public static void setButtonTintMode(@NonNull CompoundButton button,
             PorterDuff.@Nullable Mode tintMode) {
-        Api21Impl.setButtonTintMode(button, tintMode);
+        button.setButtonTintMode(tintMode);
     }
 
     /**
@@ -82,7 +82,7 @@
      * @see #setButtonTintMode(CompoundButton, PorterDuff.Mode)
      */
     public static PorterDuff.@Nullable Mode getButtonTintMode(@NonNull CompoundButton button) {
-        return Api21Impl.getButtonTintMode(button);
+        return button.getButtonTintMode();
     }
 
     /**
@@ -93,26 +93,4 @@
     public static @Nullable Drawable getButtonDrawable(@NonNull CompoundButton button) {
         return button.getButtonDrawable();
     }
-
-    static class Api21Impl {
-        private Api21Impl() {
-            // This class is not instantiable.
-        }
-
-        static void setButtonTintList(CompoundButton compoundButton, ColorStateList tint) {
-            compoundButton.setButtonTintList(tint);
-        }
-
-        static ColorStateList getButtonTintList(CompoundButton compoundButton) {
-            return compoundButton.getButtonTintList();
-        }
-
-        static void setButtonTintMode(CompoundButton compoundButton, PorterDuff.Mode tintMode) {
-            compoundButton.setButtonTintMode(tintMode);
-        }
-
-        static PorterDuff.Mode getButtonTintMode(CompoundButton compoundButton) {
-            return compoundButton.getButtonTintMode();
-        }
-    }
 }
diff --git a/core/core/src/main/res/drawable-v21/notification_action_background.xml b/core/core/src/main/res/drawable/notification_action_background.xml
similarity index 100%
rename from core/core/src/main/res/drawable-v21/notification_action_background.xml
rename to core/core/src/main/res/drawable/notification_action_background.xml
diff --git a/core/core/src/main/res/layout-v21/notification_action.xml b/core/core/src/main/res/layout-v21/notification_action.xml
deleted file mode 100644
index 7199c25..0000000
--- a/core/core/src/main/res/layout-v21/notification_action.xml
+++ /dev/null
@@ -1,41 +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
-  -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    style="@style/Widget.Compat.NotificationActionContainer"
-    android:id="@+id/action_container"
-    android:layout_width="0dp"
-    android:layout_weight="1"
-    android:layout_height="48dp"
-    android:paddingStart="4dp"
-    android:orientation="horizontal">
-    <ImageView
-        android:id="@+id/action_image"
-        android:layout_width="@dimen/notification_action_icon_size"
-        android:layout_height="@dimen/notification_action_icon_size"
-        android:layout_gravity="center|start"
-        android:scaleType="centerInside"/>
-    <TextView
-        style="@style/Widget.Compat.NotificationActionText"
-        android:id="@+id/action_text"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center|start"
-        android:paddingStart="4dp"
-        android:singleLine="true"
-        android:ellipsize="end"
-        android:clickable="false"/>
-</LinearLayout>
\ No newline at end of file
diff --git a/core/core/src/main/res/layout-v21/notification_action_tombstone.xml b/core/core/src/main/res/layout-v21/notification_action_tombstone.xml
deleted file mode 100644
index 7ef38fa..0000000
--- a/core/core/src/main/res/layout-v21/notification_action_tombstone.xml
+++ /dev/null
@@ -1,48 +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
-  -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    style="@style/Widget.Compat.NotificationActionContainer"
-    android:id="@+id/action_container"
-    android:layout_width="0dp"
-    android:layout_weight="1"
-    android:layout_height="48dp"
-    android:paddingStart="4dp"
-    android:orientation="horizontal"
-    android:enabled="false"
-    android:background="@null">
-    <ImageView
-        android:id="@+id/action_image"
-        android:layout_width="@dimen/notification_action_icon_size"
-        android:layout_height="@dimen/notification_action_icon_size"
-        android:layout_gravity="center|start"
-        android:scaleType="centerInside"
-        android:enabled="false"
-        android:alpha="0.5"/>
-    <TextView
-        style="@style/Widget.Compat.NotificationActionText"
-        android:id="@+id/action_text"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center|start"
-        android:paddingStart="4dp"
-        android:singleLine="true"
-        android:ellipsize="end"
-        android:clickable="false"
-        android:enabled="false"
-        android:alpha="0.5"/>
-</LinearLayout>
\ No newline at end of file
diff --git a/core/core/src/main/res/layout-v21/notification_template_custom_big.xml b/core/core/src/main/res/layout-v21/notification_template_custom_big.xml
deleted file mode 100644
index 9e3666e..0000000
--- a/core/core/src/main/res/layout-v21/notification_template_custom_big.xml
+++ /dev/null
@@ -1,90 +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
-  -->
-
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/notification_background"
-    android:layout_width="match_parent"
-    android:layout_height="wrap_content" >
-    <include layout="@layout/notification_template_icon_group"
-        android:layout_width="@dimen/notification_large_icon_width"
-        android:layout_height="@dimen/notification_large_icon_height"
-    />
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_gravity="top"
-        android:layout_marginStart="@dimen/notification_large_icon_width"
-        android:orientation="vertical" >
-        <LinearLayout
-            android:id="@+id/notification_main_column_container"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:minHeight="@dimen/notification_large_icon_height"
-            android:orientation="horizontal">
-            <FrameLayout
-                android:id="@+id/notification_main_column"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_weight="1"
-                android:layout_marginEnd="8dp"
-                android:layout_marginBottom="8dp"/>
-            <FrameLayout
-                android:id="@+id/right_side"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginEnd="8dp"
-                android:paddingTop="@dimen/notification_right_side_padding_top">
-                <ViewStub android:id="@+id/time"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="end|top"
-                    android:visibility="gone"
-                    android:layout="@layout/notification_template_part_time" />
-                <ViewStub android:id="@+id/chronometer"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="end|top"
-                    android:visibility="gone"
-                    android:layout="@layout/notification_template_part_chronometer" />
-                <TextView android:id="@+id/info"
-                    android:textAppearance="@style/TextAppearance.Compat.Notification.Info"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_marginTop="20dp"
-                    android:layout_gravity="end|bottom"
-                    android:singleLine="true"
-                />
-            </FrameLayout>
-        </LinearLayout>
-        <ImageView
-            android:layout_width="match_parent"
-            android:layout_height="1dp"
-            android:id="@+id/action_divider"
-            android:visibility="gone"
-            android:background="#29000000" />
-        <LinearLayout
-            android:id="@+id/actions"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginStart="-8dp"
-            android:orientation="horizontal"
-            android:visibility="gone"
-        >
-            <!-- actions will be added here -->
-        </LinearLayout>
-    </LinearLayout>
-</FrameLayout>
\ No newline at end of file
diff --git a/core/core/src/main/res/layout-v21/notification_template_icon_group.xml b/core/core/src/main/res/layout-v21/notification_template_icon_group.xml
deleted file mode 100644
index 8fadd67..0000000
--- a/core/core/src/main/res/layout-v21/notification_template_icon_group.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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.
-  -->
-
-<FrameLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="@dimen/notification_large_icon_width"
-    android:layout_height="@dimen/notification_large_icon_height"
-    android:id="@+id/icon_group"
->
-    <ImageView android:id="@+id/icon"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_marginTop="@dimen/notification_big_circle_margin"
-        android:layout_marginBottom="@dimen/notification_big_circle_margin"
-        android:layout_marginStart="@dimen/notification_big_circle_margin"
-        android:layout_marginEnd="@dimen/notification_big_circle_margin"
-        android:scaleType="centerInside"
-    />
-    <ImageView android:id="@+id/right_icon"
-        android:layout_width="@dimen/notification_right_icon_size"
-        android:layout_height="@dimen/notification_right_icon_size"
-        android:layout_gravity="end|bottom"
-        android:scaleType="centerInside"
-        android:visibility="gone"
-        android:layout_marginEnd="8dp"
-        android:layout_marginBottom="8dp"
-    />
-</FrameLayout>
diff --git a/core/core/src/main/res/layout/notification_action.xml b/core/core/src/main/res/layout/notification_action.xml
index 3ffd9a1..7199c25 100644
--- a/core/core/src/main/res/layout/notification_action.xml
+++ b/core/core/src/main/res/layout/notification_action.xml
@@ -20,7 +20,6 @@
     android:layout_width="0dp"
     android:layout_weight="1"
     android:layout_height="48dp"
-    android:paddingLeft="4dp"
     android:paddingStart="4dp"
     android:orientation="horizontal">
     <ImageView
@@ -32,11 +31,9 @@
     <TextView
         style="@style/Widget.Compat.NotificationActionText"
         android:id="@+id/action_text"
-        android:textColor="#ccc"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center|start"
-        android:paddingLeft="4dp"
         android:paddingStart="4dp"
         android:singleLine="true"
         android:ellipsize="end"
diff --git a/core/core/src/main/res/layout/notification_action_tombstone.xml b/core/core/src/main/res/layout/notification_action_tombstone.xml
index e9d4e37..7ef38fa 100644
--- a/core/core/src/main/res/layout/notification_action_tombstone.xml
+++ b/core/core/src/main/res/layout/notification_action_tombstone.xml
@@ -21,7 +21,6 @@
     android:layout_width="0dp"
     android:layout_weight="1"
     android:layout_height="48dp"
-    android:paddingLeft="4dp"
     android:paddingStart="4dp"
     android:orientation="horizontal"
     android:enabled="false"
@@ -40,8 +39,6 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center|start"
-        android:textColor="#ccc"
-        android:paddingLeft="4dp"
         android:paddingStart="4dp"
         android:singleLine="true"
         android:ellipsize="end"
diff --git a/core/core/src/main/res/layout/notification_template_custom_big.xml b/core/core/src/main/res/layout/notification_template_custom_big.xml
index d0519dc..6b8f7cf 100644
--- a/core/core/src/main/res/layout/notification_template_custom_big.xml
+++ b/core/core/src/main/res/layout/notification_template_custom_big.xml
@@ -19,23 +19,20 @@
     android:id="@+id/notification_background"
     android:layout_width="match_parent"
     android:layout_height="wrap_content" >
-    <ImageView android:id="@+id/icon"
+    <include layout="@layout/notification_template_icon_group"
         android:layout_width="@dimen/notification_large_icon_width"
         android:layout_height="@dimen/notification_large_icon_height"
-        android:scaleType="center"
-    />
+        />
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_gravity="top"
+        android:layout_marginStart="@dimen/notification_large_icon_width"
         android:orientation="vertical" >
         <LinearLayout
             android:id="@+id/notification_main_column_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_marginLeft="@dimen/notification_large_icon_width"
-            android:layout_marginStart="@dimen/notification_large_icon_width"
-            android:paddingTop="@dimen/notification_main_column_padding_top"
             android:minHeight="@dimen/notification_large_icon_height"
             android:orientation="horizontal">
             <FrameLayout
@@ -43,16 +40,12 @@
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_weight="1"
-                android:layout_marginLeft="@dimen/notification_content_margin_start"
-                android:layout_marginStart="@dimen/notification_content_margin_start"
-                android:layout_marginBottom="8dp"
-                android:layout_marginRight="8dp"
-                android:layout_marginEnd="8dp" />
+                android:layout_marginEnd="8dp"
+                android:layout_marginBottom="8dp"/>
             <FrameLayout
                 android:id="@+id/right_side"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginRight="8dp"
                 android:layout_marginEnd="8dp"
                 android:paddingTop="@dimen/notification_right_side_padding_top">
                 <ViewStub android:id="@+id/time"
@@ -67,50 +60,30 @@
                     android:layout_gravity="end|top"
                     android:visibility="gone"
                     android:layout="@layout/notification_template_part_chronometer" />
-                <LinearLayout
-                    android:layout_width="match_parent"
+                <TextView android:id="@+id/info"
+                    android:textAppearance="@style/TextAppearance.Compat.Notification.Info"
+                    android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:orientation="horizontal"
+                    android:layout_marginTop="20dp"
                     android:layout_gravity="end|bottom"
-                    android:layout_marginTop="20dp">
-                    <TextView android:id="@+id/info"
-                        android:textAppearance="@style/TextAppearance.Compat.Notification.Info"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:singleLine="true"
+                    android:singleLine="true"
                     />
-                    <ImageView android:id="@+id/right_icon"
-                        android:layout_width="16dp"
-                        android:layout_height="16dp"
-                        android:layout_gravity="center"
-                        android:layout_marginLeft="8dp"
-                        android:layout_marginStart="8dp"
-                        android:scaleType="centerInside"
-                        android:visibility="gone"
-                        android:alpha="0.6"
-                    />
-                </LinearLayout>
             </FrameLayout>
         </LinearLayout>
         <ImageView
             android:layout_width="match_parent"
-            android:layout_height="1px"
+            android:layout_height="1dp"
             android:id="@+id/action_divider"
             android:visibility="gone"
-            android:layout_marginLeft="@dimen/notification_large_icon_width"
-            android:layout_marginStart="@dimen/notification_large_icon_width"
-            android:background="?android:attr/dividerHorizontal" />
+            android:background="#29000000" />
         <LinearLayout
             android:id="@+id/actions"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
+            android:layout_marginStart="-8dp"
             android:orientation="horizontal"
             android:visibility="gone"
-            android:showDividers="middle"
-            android:divider="?android:attr/listDivider"
-            android:dividerPadding="12dp"
-            android:layout_marginLeft="@dimen/notification_large_icon_width"
-            android:layout_marginStart="@dimen/notification_large_icon_width" >
+            >
             <!-- actions will be added here -->
         </LinearLayout>
     </LinearLayout>
diff --git a/core/core/src/main/res/layout/notification_template_icon_group.xml b/core/core/src/main/res/layout/notification_template_icon_group.xml
index f225737..ddb4587 100644
--- a/core/core/src/main/res/layout/notification_template_icon_group.xml
+++ b/core/core/src/main/res/layout/notification_template_icon_group.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
-  ~ Copyright (C) 2016 The Android Open Source Project
+  ~ 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.
@@ -12,13 +12,31 @@
   ~ distributed under the License is distributed on an "AS IS" BASIS,
   ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   ~ See the License for the specific language governing permissions and
-  ~ limitations under the License
+  ~ limitations under the License.
   -->
 
-<ImageView
+<FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/icon"
     android:layout_width="@dimen/notification_large_icon_width"
     android:layout_height="@dimen/notification_large_icon_height"
-    android:scaleType="centerCrop"
-/>
+    android:id="@+id/icon_group"
+    >
+    <ImageView android:id="@+id/icon"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginTop="@dimen/notification_big_circle_margin"
+        android:layout_marginBottom="@dimen/notification_big_circle_margin"
+        android:layout_marginStart="@dimen/notification_big_circle_margin"
+        android:layout_marginEnd="@dimen/notification_big_circle_margin"
+        android:scaleType="centerInside"
+        />
+    <ImageView android:id="@+id/right_icon"
+        android:layout_width="@dimen/notification_right_icon_size"
+        android:layout_height="@dimen/notification_right_icon_size"
+        android:layout_gravity="end|bottom"
+        android:scaleType="centerInside"
+        android:visibility="gone"
+        android:layout_marginEnd="8dp"
+        android:layout_marginBottom="8dp"
+        />
+</FrameLayout>
diff --git a/core/core/src/main/res/values-v21/colors.xml b/core/core/src/main/res/values-v21/colors.xml
deleted file mode 100644
index b093f0d..0000000
--- a/core/core/src/main/res/values-v21/colors.xml
+++ /dev/null
@@ -1,19 +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
-  -->
-<resources>
-    <color name="notification_action_color_filter">@color/androidx_core_secondary_text_default_material_light</color>
-</resources>
\ No newline at end of file
diff --git a/core/core/src/main/res/values-v21/dimens.xml b/core/core/src/main/res/values-v21/dimens.xml
deleted file mode 100644
index b56faf1..0000000
--- a/core/core/src/main/res/values-v21/dimens.xml
+++ /dev/null
@@ -1,24 +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.
--->
-
-<resources>
-    <!-- the margin at the beginning of the notification content -->
-    <dimen name="notification_content_margin_start">0dp</dimen>
-    <!-- image margin on the large icon in the narrow media template -->
-    <dimen name="notification_media_narrow_margin">12dp</dimen>
-    <!-- the top padding of the notification content -->
-    <dimen name="notification_main_column_padding_top">0dp</dimen>
-</resources>
diff --git a/core/core/src/main/res/values-v21/styles.xml b/core/core/src/main/res/values-v21/styles.xml
deleted file mode 100644
index a142274..0000000
--- a/core/core/src/main/res/values-v21/styles.xml
+++ /dev/null
@@ -1,42 +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.
-  -->
-
-<resources>
-
-    <!-- Use platform styles -->
-    <style name="TextAppearance.Compat.Notification"
-        parent="@android:style/TextAppearance.Material.Notification"/>
-
-    <style name="TextAppearance.Compat.Notification.Title"
-        parent="@android:style/TextAppearance.Material.Notification.Title"/>
-
-    <style name="TextAppearance.Compat.Notification.Info"
-        parent="@android:style/TextAppearance.Material.Notification.Info"/>
-
-    <style name="TextAppearance.Compat.Notification.Time"
-        parent="@android:style/TextAppearance.Material.Notification.Time"/>
-
-    <style name="Widget.Compat.NotificationActionText" parent="">
-        <item name="android:textAppearance">?android:attr/textAppearanceButton</item>
-        <item name="android:textColor">@color/androidx_core_secondary_text_default_material_light</item>
-        <item name="android:textSize">@dimen/notification_action_text_size</item>
-    </style>
-
-    <style name="Widget.Compat.NotificationActionContainer" parent="">
-        <item name="android:background">@drawable/notification_action_background</item>
-    </style>
-
-</resources>
diff --git a/core/core/src/main/res/values/colors.xml b/core/core/src/main/res/values/colors.xml
index cad8425..2ad5208 100644
--- a/core/core/src/main/res/values/colors.xml
+++ b/core/core/src/main/res/values/colors.xml
@@ -17,7 +17,7 @@
 <resources>
     <drawable name="notification_template_icon_bg">#3333B5E5</drawable>
     <drawable name="notification_template_icon_low_bg">#0cffffff</drawable>
-    <color name="notification_action_color_filter">#ffffffff</color>
+    <color name="notification_action_color_filter">@color/androidx_core_secondary_text_default_material_light</color>
     <color name="notification_icon_bg_color">#ff9e9e9e</color>
 
     <!-- The color of the Decline and Hang Up actions on a CallStyle notification -->
diff --git a/core/core/src/main/res/values/dimens.xml b/core/core/src/main/res/values/dimens.xml
index 36644cb..bae54ab 100644
--- a/core/core/src/main/res/values/dimens.xml
+++ b/core/core/src/main/res/values/dimens.xml
@@ -60,13 +60,13 @@
     <dimen name="notification_small_icon_size_as_large">24dp</dimen>
 
     <!-- the margin at the beginning of the notification content -->
-    <dimen name="notification_content_margin_start">8dp</dimen>
+    <dimen name="notification_content_margin_start">0dp</dimen>
 
     <!-- image margin on the large icon in the narrow media template -->
-    <dimen name="notification_media_narrow_margin">@dimen/notification_content_margin_start</dimen>
+    <dimen name="notification_media_narrow_margin">12dp</dimen>
 
     <!-- the top padding of the notification content -->
-    <dimen name="notification_main_column_padding_top">10dp</dimen>
+    <dimen name="notification_main_column_padding_top">0dp</dimen>
 
     <!-- the paddingtop on the right side of the notification (for time etc.) -->
     <dimen name="notification_right_side_padding_top">4dp</dimen>
diff --git a/core/core/src/main/res/values/styles.xml b/core/core/src/main/res/values/styles.xml
index 3503e77..0060e41 100644
--- a/core/core/src/main/res/values/styles.xml
+++ b/core/core/src/main/res/values/styles.xml
@@ -17,22 +17,27 @@
 
 <resources>
     <style name="TextAppearance.Compat.Notification"
-        parent="@android:style/TextAppearance.StatusBar.EventContent"/>
+        parent="@android:style/TextAppearance.Material.Notification"/>
 
     <style name="TextAppearance.Compat.Notification.Title"
-        parent="@android:style/TextAppearance.StatusBar.EventContent.Title"/>
+        parent="@android:style/TextAppearance.Material.Notification.Title"/>
 
-    <style name="TextAppearance.Compat.Notification.Info">
-        <item name="android:textSize">12sp</item>
-        <item name="android:textColor">?android:attr/textColorSecondary</item>
-    </style>
-    <style name="TextAppearance.Compat.Notification.Time">
-        <item name="android:textSize">12sp</item>
-        <item name="android:textColor">?android:attr/textColorSecondary</item>
-    </style>
+    <style name="TextAppearance.Compat.Notification.Info"
+        parent="@android:style/TextAppearance.Material.Notification.Info"/>
+
+    <style name="TextAppearance.Compat.Notification.Time"
+        parent="@android:style/TextAppearance.Material.Notification.Time"/>
+
     <style name="TextAppearance.Compat.Notification.Line2"
          parent="TextAppearance.Compat.Notification.Info" />
 
-    <style name="Widget.Compat.NotificationActionText" parent=""/>
-    <style name="Widget.Compat.NotificationActionContainer" parent=""/>
+    <style name="Widget.Compat.NotificationActionText" parent="">
+        <item name="android:textAppearance">?android:attr/textAppearanceButton</item>
+        <item name="android:textColor">@color/androidx_core_secondary_text_default_material_light</item>
+        <item name="android:textSize">@dimen/notification_action_text_size</item>
+    </style>
+
+    <style name="Widget.Compat.NotificationActionContainer" parent="">
+        <item name="android:background">@drawable/notification_action_background</item>
+    </style>
 </resources>
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5266e42..f636c27 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -215,16 +215,10 @@
 kotlinGradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
 kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib" }
 kotlinStdlibCommon = { module = "org.jetbrains.kotlin:kotlin-stdlib-common" }
-# The Kotlin/JS and Kotlin/Wasm standard library must match the compiler
-kotlinStdlibJs = { module = "org.jetbrains.kotlin:kotlin-stdlib-js", version.ref = "kotlin" }
-kotlinStdlibWasm = { module = "org.jetbrains.kotlin:kotlin-stdlib-wasm-js", version.ref = "kotlin" }
 kotlinTest = { module = "org.jetbrains.kotlin:kotlin-test" }
-kotlinTestForWasmTests = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
 kotlinTestAnnotationsCommon = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common" }
 kotlinTestCommon = { module = "org.jetbrains.kotlin:kotlin-test-common" }
 kotlinTestJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit" }
-kotlinTestJs = { module = "org.jetbrains.kotlin:kotlin-test-js" }
-kotlinTestWasm = { module = "org.jetbrains.kotlin:kotlin-test-wasm-js" }
 kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect" }
 kotlinPoet = { module = "com.squareup:kotlinpoet", version = "2.1.0" }
 kotlinPoetJavaPoet = { module = "com.squareup:kotlinpoet-javapoet", version = "2.1.0" }
diff --git a/graphics/graphics-shapes/build.gradle b/graphics/graphics-shapes/build.gradle
index 88b6bef..11366d0 100644
--- a/graphics/graphics-shapes/build.gradle
+++ b/graphics/graphics-shapes/build.gradle
@@ -57,8 +57,7 @@
 
         commonTest {
             dependencies {
-                // https://youtrack.jetbrains.com/issue/KT-71032
-                implementation(libs.kotlinTestForWasmTests)
+                implementation(libs.kotlinTest)
                 implementation(project(":kruth:kruth"))
             }
         }
@@ -112,38 +111,13 @@
             dependsOn(nonJvmTest)
         }
 
-        jsMain {
-            dependsOn(nonJvmMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        jsTest {
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-                implementation(libs.kotlinTestJs)
-            }
-        }
-
-        wasmJsMain {
-            dependsOn(nonJvmMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        wasmJsTest {
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-                implementation(libs.kotlinTestWasm)
-            }
-        }
-
         targets.configureEach { target ->
             if (target.platformType == KotlinPlatformType.native) {
                 target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
                 target.compilations["test"].defaultSourceSet.dependsOn(nativeTest)
+            } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
+                target.compilations["main"].defaultSourceSet.dependsOn(nonJvmMain)
+                target.compilations["test"].defaultSourceSet.dependsOn(nonJvmTest)
             }
         }
     }
diff --git a/kruth/kruth/build.gradle b/kruth/kruth/build.gradle
index 9f95fac..7a7d860 100644
--- a/kruth/kruth/build.gradle
+++ b/kruth/kruth/build.gradle
@@ -104,33 +104,11 @@
         webTest {
             dependsOn(nonJvmTest)
             dependencies {
-                // https://youtrack.jetbrains.com/issue/KT-71032
-                implementation(libs.kotlinTestForWasmTests)
                 implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
                 implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
             }
         }
 
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-        jsTest {
-            dependsOn(webTest)
-        }
-
-        wasmJsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-        wasmJsTest {
-            dependsOn(webTest)
-        }
-
         nativeMain {
             dependsOn(nonJvmMain)
         }
@@ -140,12 +118,11 @@
 
         targets.configureEach { target ->
             if (target.platformType == KotlinPlatformType.native) {
-                target.compilations["main"].defaultSourceSet {
-                    dependsOn(nativeMain)
-                }
-                target.compilations["test"].defaultSourceSet {
-                    dependsOn(nativeTest)
-                }
+                target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
+                target.compilations["test"].defaultSourceSet.dependsOn(nativeTest)
+            } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
+                target.compilations["main"].defaultSourceSet.dependsOn(webMain)
+                target.compilations["test"].defaultSourceSet.dependsOn(webTest)
             }
         }
 
diff --git a/libraryversions.toml b/libraryversions.toml
index 324d1f1..d10a78c 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -46,7 +46,7 @@
 CORE_REMOTEVIEWS = "1.1.0-rc01"
 CORE_ROLE = "1.2.0-alpha01"
 CORE_SPLASHSCREEN = "1.2.0-rc01"
-CORE_TELECOM = "1.0.0-beta01"
+CORE_TELECOM = "1.1.0-alpha01"
 CORE_UWB = "1.0.0-alpha10"
 CORE_VIEWTREE = "1.1.0-alpha01"
 CREDENTIALS = "1.6.0-alpha05"
@@ -183,18 +183,17 @@
 WEAR_WATCHFACEPUSH = "1.0.0-alpha01"
 WEBKIT = "1.15.0-alpha02"
 # Adding a comment to prevent merge conflicts for Window artifact
-WINDOW = "1.5.0-beta02"
+WINDOW = "1.5.0-rc01"
 WINDOW_EXTENSIONS = "1.5.0"
 WINDOW_EXTENSIONS_CORE = "1.1.0-alpha01"
 WINDOW_SIDECAR = "1.0.0-rc01"
 WORK = "2.11.0-alpha01"
-XR = "1.0.0-alpha05"
-XR_ARCORE = "1.0.0-alpha05"
-XR_COMPOSE = "1.0.0-alpha06"
+XR_ARCORE = "1.0.0-alpha06"
+XR_COMPOSE = "1.0.0-alpha07"
 XR_GLIMMER = "1.0.0-alpha01"
 XR_PROJECTED = "1.0.0-alpha01"
-XR_RUNTIME = "1.0.0-alpha05"
-XR_SCENECORE = "1.0.0-alpha06"
+XR_RUNTIME = "1.0.0-alpha06"
+XR_SCENECORE = "1.0.0-alpha07"
 
 [groups]
 ACTIVITY = { group = "androidx.activity", atomicGroupVersion = "versions.ACTIVITY" }
@@ -336,7 +335,6 @@
 WINDOW_EXTENSIONS_CORE = { group = "androidx.window.extensions.core", atomicGroupVersion = "versions.WINDOW_EXTENSIONS_CORE" }
 WINDOW_SIDECAR = { group = "androidx.window.sidecar", atomicGroupVersion = "versions.WINDOW_SIDECAR" }
 WORK = { group = "androidx.work", atomicGroupVersion = "versions.WORK" }
-XR = { group = "androidx.xr", atomicGroupVersion = "versions.XR" }
 XR_ARCORE = { group = "androidx.xr.arcore", atomicGroupVersion = "versions.XR_ARCORE" }
 XR_COMPOSE = { group = "androidx.xr.compose", atomicGroupVersion = "versions.XR_COMPOSE" }
 XR_GLIMMER = { group = "androidx.xr.glimmer", atomicGroupVersion = "versions.XR_GLIMMER" }
diff --git a/lifecycle/lifecycle-common/build.gradle b/lifecycle/lifecycle-common/build.gradle
index d6ae520..71ebbbe 100644
--- a/lifecycle/lifecycle-common/build.gradle
+++ b/lifecycle/lifecycle-common/build.gradle
@@ -73,20 +73,6 @@
             }
         }
 
-        wasmJsMain {
-            dependsOn(nonJvmMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        jsMain {
-            dependsOn(nonJvmMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
         targets.configureEach { target ->
             if (target.platformType !in [KotlinPlatformType.jvm, KotlinPlatformType.common]) {
                 target.compilations["main"].defaultSourceSet.dependsOn(nonJvmMain)
diff --git a/lifecycle/lifecycle-runtime-compose/build.gradle b/lifecycle/lifecycle-runtime-compose/build.gradle
index 452984a..dd0c22a 100644
--- a/lifecycle/lifecycle-runtime-compose/build.gradle
+++ b/lifecycle/lifecycle-runtime-compose/build.gradle
@@ -122,36 +122,6 @@
             dependsOn(nonAndroidTest)
         }
 
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        jsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-                implementation(libs.kotlinTestJs)
-            }
-        }
-
-        wasmJsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        wasmJsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-                implementation(libs.kotlinTestWasm)
-            }
-        }
-
         targets.configureEach { target ->
             if (target.platformType == KotlinPlatformType.native) {
                 target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
diff --git a/lifecycle/lifecycle-runtime-testing/build.gradle b/lifecycle/lifecycle-runtime-testing/build.gradle
index 5907e29..958c4af 100644
--- a/lifecycle/lifecycle-runtime-testing/build.gradle
+++ b/lifecycle/lifecycle-runtime-testing/build.gradle
@@ -117,36 +117,6 @@
             }
         }
 
-        wasmJsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        wasmJsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-                implementation(libs.kotlinTestForWasmTests)
-            }
-        }
-
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        jsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-                implementation(libs.kotlinTestJs)
-            }
-        }
-
         targets.configureEach { target ->
             if (target.platformType == KotlinPlatformType.native) {
                 target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
diff --git a/lifecycle/lifecycle-runtime/build.gradle b/lifecycle/lifecycle-runtime/build.gradle
index a8eedbb..7c0846f 100644
--- a/lifecycle/lifecycle-runtime/build.gradle
+++ b/lifecycle/lifecycle-runtime/build.gradle
@@ -146,36 +146,6 @@
             }
         }
 
-        wasmJsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        wasmJsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-                implementation(libs.kotlinTestForWasmTests)
-            }
-        }
-
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        jsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-                implementation(libs.kotlinTestJs)
-            }
-        }
-
         targets.configureEach { target ->
             if (target.platformType == KotlinPlatformType.native) {
                 target.compilations["main"].defaultSourceSet {
diff --git a/lifecycle/lifecycle-runtime/src/jvmMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt b/lifecycle/lifecycle-runtime/src/jvmMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt
index 798d9da..c962d7a 100644
--- a/lifecycle/lifecycle-runtime/src/jvmMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt
+++ b/lifecycle/lifecycle-runtime/src/jvmMain/kotlin/androidx/lifecycle/LifecycleRegistry.jvm.kt
@@ -123,6 +123,7 @@
         if (state == next) {
             return
         }
+        @Suppress("NewApi") // b/437073246
         checkLifecycleStateTransition(lifecycleOwner.get(), state, next)
 
         state = next
@@ -176,6 +177,7 @@
         if (previous != null) {
             return
         }
+        @Suppress("NewApi") // b/437073246
         val lifecycleOwner =
             lifecycleOwner.get()
                 ?: // it is null we should be destroyed. Fallback quickly
@@ -272,6 +274,7 @@
     // happens only on the top of stack (never in reentrance),
     // so it doesn't have to take in account parents
     private fun sync() {
+        @Suppress("NewApi") // b/437073246
         val lifecycleOwner =
             lifecycleOwner.get()
                 ?: throw IllegalStateException(
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
index 08615f5..4c2a6fe 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
@@ -56,7 +56,7 @@
         commonMain {
             dependencies {
                 api("androidx.annotation:annotation:1.9.1")
-                api(project(":savedstate:savedstate"))
+                api("androidx.savedstate:savedstate:1.4.0-alpha02")
                 api(project(":lifecycle:lifecycle-viewmodel"))
                 implementation(project(":lifecycle:lifecycle-common"))
                 api(libs.kotlinStdlib)
@@ -172,36 +172,6 @@
             }
         }
 
-        wasmJsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        wasmJsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-                implementation(libs.kotlinTestForWasmTests)
-            }
-        }
-
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        jsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-                implementation(libs.kotlinTestJs)
-            }
-        }
-
         targets.configureEach { target ->
             if (target.platformType == KotlinPlatformType.native) {
                 if (target.konanTarget.family.appleFamily) {
diff --git a/lifecycle/lifecycle-viewmodel-testing/build.gradle b/lifecycle/lifecycle-viewmodel-testing/build.gradle
index 01062cf..86b8630 100644
--- a/lifecycle/lifecycle-viewmodel-testing/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-testing/build.gradle
@@ -163,36 +163,6 @@
             }
         }
 
-        wasmJsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        wasmJsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-                implementation(libs.kotlinTestForWasmTests)
-            }
-        }
-
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        jsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-                implementation(libs.kotlinTestJs)
-            }
-        }
-
         targets.configureEach { target ->
             if (target.platformType == KotlinPlatformType.native) {
                 if (target.konanTarget.family.appleFamily) {
diff --git a/lifecycle/lifecycle-viewmodel/build.gradle b/lifecycle/lifecycle-viewmodel/build.gradle
index 5636018..d2c81ec 100644
--- a/lifecycle/lifecycle-viewmodel/build.gradle
+++ b/lifecycle/lifecycle-viewmodel/build.gradle
@@ -144,36 +144,6 @@
             }
         }
 
-        wasmJsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        wasmJsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-                implementation(libs.kotlinTestForWasmTests)
-            }
-        }
-
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        jsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-                implementation(libs.kotlinTestJs)
-            }
-        }
-
         targets.configureEach { target ->
             if (target.platformType == KotlinPlatformType.native) {
                 if (target.konanTarget.family.appleFamily) {
diff --git a/mediarouter/mediarouter/lint-baseline.xml b/mediarouter/mediarouter/lint-baseline.xml
index be36a5a..19e2e94 100644
--- a/mediarouter/mediarouter/lint-baseline.xml
+++ b/mediarouter/mediarouter/lint-baseline.xml
@@ -46,22 +46,4 @@
             file="src/main/java/androidx/mediarouter/media/RemotePlaybackClient.java"/>
     </issue>
 
-    <issue
-        id="ObsoleteSdkInt"
-        message="Unnecessary; `SDK_INT` is always >= 23"
-        errorLine1="        if (Build.VERSION.SDK_INT >= 23) {"
-        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java"/>
-    </issue>
-
-    <issue
-        id="ObsoleteSdkInt"
-        message="Unnecessary; `SDK_INT` is always >= 23"
-        errorLine1="    @RequiresApi(23)"
-        errorLine2="    ~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java"/>
-    </issue>
-
 </issues>
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 4a4e65c..768857d 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/SystemOutputSwitcherDialogController.java
@@ -203,10 +203,26 @@
 
     private static boolean isSuitableDeviceAlreadyConnectedAsAudioOutput(
             @NonNull Context context) {
-        if (Build.VERSION.SDK_INT >= 23) {
-            return Api23Impl.isSuitableDeviceAlreadyConnectedAsAudioOutput(context);
+        AudioManager audioManager = context.getSystemService(AudioManager.class);
+        AudioDeviceInfo[] audioDeviceInfos = audioManager.getDevices(
+                AudioManager.GET_DEVICES_OUTPUTS);
+        for (AudioDeviceInfo device : audioDeviceInfos) {
+            switch (device.getType()) {
+                case AudioDeviceInfo.TYPE_BLE_BROADCAST:
+                case AudioDeviceInfo.TYPE_BLE_HEADSET:
+                case AudioDeviceInfo.TYPE_BLE_SPEAKER:
+                case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
+                case AudioDeviceInfo.TYPE_HEARING_AID:
+                case AudioDeviceInfo.TYPE_LINE_ANALOG:
+                case AudioDeviceInfo.TYPE_LINE_DIGITAL:
+                case AudioDeviceInfo.TYPE_USB_DEVICE:
+                case AudioDeviceInfo.TYPE_USB_HEADSET:
+                case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
+                case AudioDeviceInfo.TYPE_WIRED_HEADSET:
+                    return true;
+            }
         }
-        return true;
+        return false;
     }
 
     @RequiresApi(30)
@@ -230,33 +246,4 @@
             return mediaRouter2.showSystemOutputSwitcher();
         }
     }
-
-    @RequiresApi(23)
-    static final class Api23Impl {
-        private Api23Impl() {
-        }
-
-        public static boolean isSuitableDeviceAlreadyConnectedAsAudioOutput(Context context) {
-            AudioManager audioManager = context.getSystemService(AudioManager.class);
-            AudioDeviceInfo[] audioDeviceInfos = audioManager.getDevices(
-                    AudioManager.GET_DEVICES_OUTPUTS);
-            for (AudioDeviceInfo device : audioDeviceInfos) {
-                switch (device.getType()) {
-                    case AudioDeviceInfo.TYPE_BLE_BROADCAST:
-                    case AudioDeviceInfo.TYPE_BLE_HEADSET:
-                    case AudioDeviceInfo.TYPE_BLE_SPEAKER:
-                    case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP:
-                    case AudioDeviceInfo.TYPE_HEARING_AID:
-                    case AudioDeviceInfo.TYPE_LINE_ANALOG:
-                    case AudioDeviceInfo.TYPE_LINE_DIGITAL:
-                    case AudioDeviceInfo.TYPE_USB_DEVICE:
-                    case AudioDeviceInfo.TYPE_USB_HEADSET:
-                    case AudioDeviceInfo.TYPE_WIRED_HEADPHONES:
-                    case AudioDeviceInfo.TYPE_WIRED_HEADSET:
-                        return true;
-                }
-            }
-            return false;
-        }
-    }
 }
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 66554c9..d175ada 100644
--- a/mediarouter/mediarouter/src/test/java/androidx/mediarouter/app/SystemOutputSwitcherDialogControllerTest.java
+++ b/mediarouter/mediarouter/src/test/java/androidx/mediarouter/app/SystemOutputSwitcherDialogControllerTest.java
@@ -35,7 +35,6 @@
 import android.provider.Settings;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
 import androidx.test.core.app.ApplicationProvider;
 
 import com.google.common.collect.ImmutableList;
@@ -96,7 +95,6 @@
         }
     }
 
-    @RequiresApi(23)
     private static void setupConnectedAudioOutput(int... deviceTypes) {
         ShadowAudioManager shadowAudioManager = shadowOf(
                 ApplicationProvider.getApplicationContext().getSystemService(AudioManager.class));
diff --git a/navigation/navigation-safe-args-gradle-plugin/build.gradle b/navigation/navigation-safe-args-gradle-plugin/build.gradle
index 478b598..895151a 100644
--- a/navigation/navigation-safe-args-gradle-plugin/build.gradle
+++ b/navigation/navigation-safe-args-gradle-plugin/build.gradle
@@ -39,7 +39,7 @@
 }
 
 dependencies {
-    compileOnly("com.android.tools.build:gradle:8.1.1")
+    compileOnly("com.android.tools.build:gradle:8.4.2")
     implementation(libs.kotlinStdlib)
     implementation(project(":navigation:navigation-safe-args-generator"))
     implementation(gradleApi())
@@ -49,10 +49,10 @@
     testImplementation(project(":internal-testutils-gradle-plugin"))
     testImplementation(libs.hamcrestCore)
     testImplementation(libs.junit)
-    testPlugin("com.android.tools.build:gradle:8.1.1")
-    testPlugin("com.android.tools.build:aapt2:8.1.1-10154469")
-    testPlugin("com.android.tools.build:aapt2:8.1.1-10154469:linux")
-    testPlugin("com.android.tools.build:aapt2:8.1.1-10154469:osx")
+    testPlugin("com.android.tools.build:gradle:8.4.2")
+    testPlugin("com.android.tools.build:aapt2:8.4.2-11315950")
+    testPlugin("com.android.tools.build:aapt2:8.4.2-11315950:linux")
+    testPlugin("com.android.tools.build:aapt2:8.4.2-11315950:osx")
     testPlugin(libs.kotlinGradlePlugin)
 }
 
diff --git a/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt b/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
index 67a1adf..7e33e09 100644
--- a/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
+++ b/navigation/navigation-safe-args-gradle-plugin/src/main/kotlin/androidx/navigation/safeargs/gradle/SafeArgsPlugin.kt
@@ -97,10 +97,10 @@
         val componentsExtension =
             project.extensions.findByType(AndroidComponentsExtension::class.java)
                 ?: throw GradleException("safeargs plugin must be used with android plugin")
-        if (componentsExtension.pluginVersion < AndroidPluginVersion(7, 3)) {
+        if (componentsExtension.pluginVersion < AndroidPluginVersion(8, 4)) {
             throw GradleException(
                 "safeargs Gradle plugin is only compatible with Android " +
-                    "Gradle plugin (AGP) version 7.3.0 or higher (found " +
+                    "Gradle plugin (AGP) version 8.4.0 or higher (found " +
                     "${componentsExtension.pluginVersion})."
             )
         }
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 c99274e..6964918 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
@@ -75,8 +75,6 @@
                 .withPluginClasspath()
                 // b/175897186 set explicit metaspace size in hopes of fewer crashes
                 .withArguments("-Dorg.gradle.jvmargs=-XX:MaxMetaspaceSize=512m", *args)
-        // Run tests using Gradle 8.14 to support AGP version used for the tests, b/431847270
-        projectSetup.setUpGradleVersion(runner, "8.14")
         return runner
     }
 
diff --git a/navigation/navigation-safe-args-gradle-plugin/src/test/test-data/multimodule-project/library/src/main/AndroidManifest.xml b/navigation/navigation-safe-args-gradle-plugin/src/test/test-data/multimodule-project/library/src/main/AndroidManifest.xml
index 3e8a0a8..8b071d5 100644
--- a/navigation/navigation-safe-args-gradle-plugin/src/test/test-data/multimodule-project/library/src/main/AndroidManifest.xml
+++ b/navigation/navigation-safe-args-gradle-plugin/src/test/test-data/multimodule-project/library/src/main/AndroidManifest.xml
@@ -14,5 +14,4 @@
   ~ limitations under the License.
   -->
 
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="safe.gradle.test.app.library" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
diff --git a/navigation3/navigation3-runtime/lint-baseline.xml b/navigation3/navigation3-runtime/lint-baseline.xml
deleted file mode 100644
index cf555c9..0000000
--- a/navigation3/navigation3-runtime/lint-baseline.xml
+++ /dev/null
@@ -1,130 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.13.0-alpha01" type="baseline" client="gradle" dependencies="false" name="AGP (8.13.0-alpha01)" variant="all" version="8.13.0-alpha01">
-
-    <issue
-        id="MutableCollectionMutableState"
-        message="Creating a MutableState object with a mutable collection type"
-        errorLine1="            currStateMap = remember { mutableStateOf(stateMap1) }"
-        errorLine2="                                      ~~~~~~~~~~~~~~">
-        <location
-            file="src/androidInstrumentedTest/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProviderTest.kt"/>
-    </issue>
-
-    <issue
-        id="ListIterator"
-        message="Creating an unnecessary Iterator to iterate through a List"
-        errorLine1="        backStack.mapIndexed { index, key ->"
-        errorLine2="                  ~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
-    </issue>
-
-    <issue
-        id="ListIterator"
-        message="Creating an unnecessary Iterator to iterate through a List"
-        errorLine1="                            latestDecorators.reversed().forEach { it.onPop(contentKey) }"
-        errorLine2="                                             ~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
-    </issue>
-
-    <issue
-        id="ListIterator"
-        message="Creating an unnecessary Iterator to iterate through a List"
-        errorLine1="                            latestDecorators.reversed().forEach { it.onPop(contentKey) }"
-        errorLine2="                                                        ~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
-    </issue>
-
-    <issue
-        id="ListIterator"
-        message="Creating an unnecessary Iterator to iterate through a List"
-        errorLine1="    val latestBackStack by rememberUpdatedState(entries.map { it.contentKey })"
-        errorLine2="                                                        ~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
-    </issue>
-
-    <issue
-        id="ListIterator"
-        message="Creating an unnecessary Iterator to iterate through a List"
-        errorLine1="    latestBackStack.forEach { contentKey ->"
-        errorLine2="                    ~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
-    </issue>
-
-    <issue
-        id="ListIterator"
-        message="Creating an unnecessary Iterator to iterate through a List"
-        errorLine1="                    latestDecorators.reversed().forEach { it.onPop(contentKey) }"
-        errorLine2="                                     ~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
-    </issue>
-
-    <issue
-        id="ListIterator"
-        message="Creating an unnecessary Iterator to iterate through a List"
-        errorLine1="                    latestDecorators.reversed().forEach { it.onPop(contentKey) }"
-        errorLine2="                                                ~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
-    </issue>
-
-    <issue
-        id="ListIterator"
-        message="Creating an unnecessary Iterator to iterate through a List"
-        errorLine1="        save = { list -> list.map { encodeToSavedState(serializer, it) } },"
-        errorLine2="                              ~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/navigation3/runtime/NavBackStack.android.kt"/>
-    </issue>
-
-    <issue
-        id="ListIterator"
-        message="Creating an unnecessary Iterator to iterate through a List"
-        errorLine1="        restore = { list -> list.map { decodeFromSavedState(serializer, it) } },"
-        errorLine2="                                 ~~~">
-        <location
-            file="src/androidMain/kotlin/androidx/navigation3/runtime/NavBackStack.android.kt"/>
-    </issue>
-
-    <issue
-        id="ListIterator"
-        message="Creating an unnecessary Iterator to iterate through a List"
-        errorLine1="        .distinct()"
-        errorLine2="         ~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/navigation3/runtime/NavEntryDecorator.kt"/>
-    </issue>
-
-    <issue
-        id="UnnecessaryLambdaCreation"
-        message="Creating an unnecessary lambda to emit a captured lambda"
-        errorLine1="    CompositionLocalProvider(LocalNavEntryDecoratorLocalInfo provides localInfo) { content() }"
-        errorLine2="                                                                                   ~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInCollection"
-        message="field popCallbacks with type LinkedHashMap&lt;Integer, Function1&lt;Object, Unit>>: replace with IntObjectMap"
-        errorLine1="    val popCallbacks: LinkedHashMap&lt;Int, (key: Any) -> Unit> = LinkedHashMap()"
-        errorLine2="                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
-    </issue>
-
-    <issue
-        id="PrimitiveInCollection"
-        message="return type LinkedHashMap&lt;Integer, Function1&lt;Object, Unit>> of getPopCallbacks: replace with IntObjectMap"
-        errorLine1="    val popCallbacks: LinkedHashMap&lt;Int, (key: Any) -> Unit> = LinkedHashMap()"
-        errorLine2="                      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt"/>
-    </issue>
-
-</issues>
diff --git a/navigation3/navigation3-runtime/samples/build.gradle b/navigation3/navigation3-runtime/samples/build.gradle
index 2c5af81..4ff119f 100644
--- a/navigation3/navigation3-runtime/samples/build.gradle
+++ b/navigation3/navigation3-runtime/samples/build.gradle
@@ -50,7 +50,7 @@
     implementation("androidx.lifecycle:lifecycle-viewmodel:2.8.7")
     implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
     implementation(libs.kotlinSerializationCore)
-    implementation(project(":savedstate:savedstate-ktx"))
+    implementation("androidx.savedstate:savedstate-ktx:1.4.0-alpha02")
     implementation(project(":navigation3:navigation3-runtime"))
 
 }
diff --git a/navigation3/navigation3-runtime/src/androidInstrumentedTest/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProviderTest.kt b/navigation3/navigation3-runtime/src/androidInstrumentedTest/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProviderTest.kt
index 1947cff..d78f601 100644
--- a/navigation3/navigation3-runtime/src/androidInstrumentedTest/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProviderTest.kt
+++ b/navigation3/navigation3-runtime/src/androidInstrumentedTest/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProviderTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.navigation3.runtime
 
+import android.annotation.SuppressLint
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateListOf
 import androidx.compose.runtime.mutableStateOf
@@ -714,6 +715,7 @@
                 entry.Content()
             }
 
+        @SuppressLint("MutableCollectionMutableState")
         composeTestRule.setContent {
             currStateMap = remember { mutableStateOf(stateMap1) }
             backStackState = remember { mutableStateOf(1) }
diff --git a/navigation3/navigation3-runtime/src/androidMain/kotlin/androidx/navigation3/runtime/NavBackStack.android.kt b/navigation3/navigation3-runtime/src/androidMain/kotlin/androidx/navigation3/runtime/NavBackStack.android.kt
index f8cd97d..e9d8902 100644
--- a/navigation3/navigation3-runtime/src/androidMain/kotlin/androidx/navigation3/runtime/NavBackStack.android.kt
+++ b/navigation3/navigation3-runtime/src/androidMain/kotlin/androidx/navigation3/runtime/NavBackStack.android.kt
@@ -60,8 +60,8 @@
     serializer: KSerializer<T> = UnsafePolymorphicSerializer()
 ) =
     listSaver<List<T>, SavedState>(
-        save = { list -> list.map { encodeToSavedState(serializer, it) } },
-        restore = { list -> list.map { decodeFromSavedState(serializer, it) } },
+        save = { list -> list.fastMapOrMap { encodeToSavedState(serializer, it) } },
+        restore = { list -> list.fastMapOrMap { decodeFromSavedState(serializer, it) } },
     )
 
 @Suppress("UNCHECKED_CAST")
diff --git a/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt b/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt
index 647a2b8..bf483dd 100644
--- a/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt
+++ b/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/DecoratedNavEntryProvider.kt
@@ -52,7 +52,7 @@
     // to ensure our lambda below takes the correct type
     entryProvider as (T) -> NavEntry<T>
     val entries =
-        backStack.map { key ->
+        backStack.fastMapOrMap { key ->
             val entry = entryProvider.invoke(key)
             decorateEntry(entry, entryDecorators)
         }
@@ -94,7 +94,9 @@
                             // onDispose
                             // calls for clean up
                             // convert to mutableList first for backwards compat.
-                            latestDecorators.reversed().forEach { it.onPop(contentKey) }
+                            latestDecorators.fastForEachReversedOrForEachReversed {
+                                it.onPop(contentKey)
+                            }
                         }
                     }
                 }
@@ -125,13 +127,13 @@
     // if an entry has been popped
     val latestEntries by rememberUpdatedState(entries)
     val latestDecorators by rememberUpdatedState(decorators)
-    entries.forEach {
+    entries.fastForEachOrForEach {
         val contentKey = it.contentKey
         contentKeys.add(contentKey)
 
         DisposableEffect(contentKey) {
             onDispose {
-                val latestBackStack = latestEntries.map { it.contentKey }
+                val latestBackStack = latestEntries.fastMapOrMap { entry -> entry.contentKey }
                 val popped =
                     if (!latestBackStack.contains(contentKey)) {
                         contentKeys.remove(contentKey)
@@ -141,18 +143,17 @@
                     // we reverse the order before popping to imitate the order
                     // of onDispose calls if each scope/decorator had their own onDispose
                     // calls for clean up
-                    latestDecorators.reversed().forEach { it.onPop(contentKey) }
+                    latestDecorators.fastForEachReversedOrForEachReversed { it.onPop(contentKey) }
                 }
             }
         }
     }
-    CompositionLocalProvider(LocalNavEntryDecoratorLocalInfo provides localInfo) { content() }
+    CompositionLocalProvider(LocalNavEntryDecoratorLocalInfo provides localInfo, content)
 }
 
 private class NavEntryDecoratorLocalInfo {
     val contentKeys: MutableSet<Any> = mutableSetOf()
     val idsInComposition: MutableSet<Any> = mutableSetOf()
-    val popCallbacks: LinkedHashMap<Int, (key: Any) -> Unit> = LinkedHashMap()
 }
 
 private val LocalNavEntryDecoratorLocalInfo =
diff --git a/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/ListUtils.kt b/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/ListUtils.kt
new file mode 100644
index 0000000..4ffaf4c
--- /dev/null
+++ b/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/ListUtils.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.navigation3.runtime
+
+import androidx.collection.MutableScatterSet
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+
+/** Internal util to use compose ui's fastX iterator apis whenever possible */
+internal inline fun <T, R> List<T>.fastMapOrMap(transform: (T) -> R) =
+    if (this is RandomAccess) {
+        this.fastMap(transform)
+    } else {
+        @Suppress("ListIterator") this.map(transform)
+    }
+
+internal inline fun <T> List<T>.fastForEachReversedOrForEachReversed(action: (T) -> Unit) {
+    if (this is RandomAccess) {
+        this.fastForEachReversed(action)
+    } else {
+        @Suppress("ListIterator") this.reversed().forEach(action)
+    }
+}
+
+internal inline fun <T> List<T>.fastForEachOrForEach(action: (T) -> Unit) {
+    if (this is RandomAccess) {
+        this.fastForEach(action)
+    } else {
+        @Suppress("ListIterator") this.forEach(action)
+    }
+}
+
+internal fun <T> List<T>.fastDistinctOrDistinct(): List<T> =
+    if (this is RandomAccess) {
+        this.fastDistinctBy { it }
+    } else {
+        @Suppress("ListIterator") this.distinct()
+    }
+
+/** Helpers copied from compose:ui:ui-util to prevent adding dep on ui-util */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+private inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
+    contract { callsInPlace(action) }
+    for (index in indices) {
+        val item = get(index)
+        action(item)
+    }
+}
+
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+private inline fun <T, R> List<T>.fastMap(transform: (T) -> R): List<R> {
+    contract { callsInPlace(transform) }
+    val target = ArrayList<R>(size)
+    fastForEach { target += transform(it) }
+    return target
+}
+
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+private inline fun <T> List<T>.fastForEachReversed(action: (T) -> Unit) {
+    contract { callsInPlace(action) }
+    for (index in indices.reversed()) {
+        val item = get(index)
+        action(item)
+    }
+}
+
+@Suppress("BanInlineOptIn") // Treat Kotlin Contracts as non-experimental.
+@OptIn(ExperimentalContracts::class)
+private inline fun <T, K> List<T>.fastDistinctBy(selector: (T) -> K): List<T> {
+    contract { callsInPlace(selector) }
+    val set = MutableScatterSet<K>(size)
+    val target = ArrayList<T>(size)
+    fastForEach { e ->
+        val key = selector(e)
+        if (set.add(key)) target += e
+    }
+    return target
+}
diff --git a/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/NavEntryDecorator.kt b/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/NavEntryDecorator.kt
index 0aa542b..6e54a59 100644
--- a/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/NavEntryDecorator.kt
+++ b/navigation3/navigation3-runtime/src/commonMain/kotlin/androidx/navigation3/runtime/NavEntryDecorator.kt
@@ -83,7 +83,7 @@
 ) {
     @Suppress("UNCHECKED_CAST")
     (entryDecorators as List<@JvmSuppressWildcards NavEntryDecorator<T>>)
-        .distinct()
+        .fastDistinctOrDistinct()
         .foldRight(initial = entry) { decorator, wrappedEntry ->
             object : NavEntryWrapper<T>(wrappedEntry) {
                 @Composable
diff --git a/navigation3/navigation3-ui/src/androidInstrumentedTest/kotlin/androidx/navigation3/ui/NavDisplayPredictiveBackTest.kt b/navigation3/navigation3-ui/src/androidInstrumentedTest/kotlin/androidx/navigation3/ui/NavDisplayPredictiveBackTest.kt
index 2a126db..27e810b 100644
--- a/navigation3/navigation3-ui/src/androidInstrumentedTest/kotlin/androidx/navigation3/ui/NavDisplayPredictiveBackTest.kt
+++ b/navigation3/navigation3-ui/src/androidInstrumentedTest/kotlin/androidx/navigation3/ui/NavDisplayPredictiveBackTest.kt
@@ -31,7 +31,7 @@
 import androidx.compose.ui.test.onNodeWithText
 import androidx.kruth.assertThat
 import androidx.navigation3.runtime.NavEntry
-import androidx.navigationevent.DirectNavigationEventInputHandler
+import androidx.navigationevent.DirectNavigationEventInput
 import androidx.navigationevent.NavigationEvent
 import androidx.navigationevent.NavigationEventDispatcher
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -131,13 +131,13 @@
         lateinit var numberOnScreen1: MutableState<Int>
         lateinit var numberOnScreen2: MutableState<Int>
         lateinit var navEventDispatcher: NavigationEventDispatcher
-        lateinit var inputHandler: DirectNavigationEventInputHandler
+        lateinit var input: DirectNavigationEventInput
         lateinit var backStack: MutableList<Any>
         composeTestRule.setContent {
             navEventDispatcher =
                 LocalNavigationEventDispatcherOwner.current!!.navigationEventDispatcher
-            inputHandler = DirectNavigationEventInputHandler()
-            navEventDispatcher.addInputHandler(inputHandler)
+            input = DirectNavigationEventInput()
+            navEventDispatcher.addInput(input)
             backStack = remember { mutableStateListOf(first) }
             NavDisplay(
                 backStack = backStack,
@@ -188,13 +188,13 @@
         assertThat(composeTestRule.onNodeWithText("numberOnScreen2: 4").isDisplayed()).isTrue()
 
         composeTestRule.runOnIdle {
-            inputHandler.handleOnStarted(NavigationEvent(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
-            inputHandler.handleOnProgressed(NavigationEvent(0.1F, 0.1F, 0.5F, BackEvent.EDGE_LEFT))
+            input.handleOnStarted(NavigationEvent(0.1F, 0.1F, 0.1F, BackEvent.EDGE_LEFT))
+            input.handleOnProgressed(NavigationEvent(0.1F, 0.1F, 0.5F, BackEvent.EDGE_LEFT))
         }
 
         composeTestRule.waitForIdle()
 
-        composeTestRule.runOnIdle { inputHandler.handleOnCompleted() }
+        composeTestRule.runOnIdle { input.handleOnCompleted() }
 
         composeTestRule.runOnIdle {
             assertWithMessage("The number should be restored")
diff --git a/navigationevent/navigationevent-compose/build.gradle b/navigationevent/navigationevent-compose/build.gradle
index 344f1be..0dc103e 100644
--- a/navigationevent/navigationevent-compose/build.gradle
+++ b/navigationevent/navigationevent-compose/build.gradle
@@ -24,6 +24,8 @@
 
 import androidx.build.SoftwareType
 import androidx.build.PlatformIdentifier
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
+import org.jetbrains.kotlin.konan.target.Family
 
 plugins {
     id("AndroidXPlugin")
@@ -153,50 +155,27 @@
         }
 
         wasmJsMain {
-            dependsOn(webMain)
             dependencies {
-                implementation(libs.kotlinStdlibWasm)
                 implementation(libs.kotlinXw3c)
             }
         }
 
-        wasmJsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinTestForWasmTests)
-            }
-        }
-
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        jsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinTestJs)
-            }
-        }
-
         targets.configureEach { target ->
-            if (target.platformType == org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.native) {
+            if (target.platformType == KotlinPlatformType.native) {
                 if (target.konanTarget.family.appleFamily) {
                     target.compilations["main"].defaultSourceSet.dependsOn(darwinMain)
                     target.compilations["test"].defaultSourceSet.dependsOn(darwinTest)
-                } else if (target.konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX) {
+                } else if (target.konanTarget.family == Family.LINUX) {
                     target.compilations["main"].defaultSourceSet.dependsOn(linuxMain)
                     target.compilations["test"].defaultSourceSet.dependsOn(linuxTest)
-                } else if (target.konanTarget.family == org.jetbrains.kotlin.konan.target.Family.MINGW) {
+                } else if (target.konanTarget.family == Family.MINGW) {
                     target.compilations["main"].defaultSourceSet.dependsOn(mingwMain)
                     target.compilations["test"].defaultSourceSet.dependsOn(mingwTest)
                 } else {
                     target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
                     target.compilations["test"].defaultSourceSet.dependsOn(nativeTest)
                 }
-            } else if (target.platformType in [org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.js, org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.wasm]) {
+            } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
                 target.compilations["main"].defaultSourceSet.dependsOn(webMain)
                 target.compilations["test"].defaultSourceSet.dependsOn(webTest)
             }
diff --git a/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwnerTest.kt b/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwnerTest.kt
index 6ff65d7..039623e 100644
--- a/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwnerTest.kt
+++ b/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwnerTest.kt
@@ -23,7 +23,7 @@
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.kruth.assertThat
 import androidx.kruth.assertThrows
-import androidx.navigationevent.DirectNavigationEventInputHandler
+import androidx.navigationevent.DirectNavigationEventInput
 import androidx.navigationevent.NavigationEventDispatcherOwner
 import androidx.navigationevent.testing.TestNavigationEventCallback
 import androidx.navigationevent.testing.TestNavigationEventDispatcherOwner
@@ -58,9 +58,9 @@
         }
 
         childOwner.navigationEventDispatcher.addCallback(callback)
-        val inputHandler = DirectNavigationEventInputHandler()
-        childOwner.navigationEventDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnCompleted()
+        val input = DirectNavigationEventInput()
+        childOwner.navigationEventDispatcher.addInput(input)
+        input.handleOnCompleted()
 
         // Verify that the child created its own, separate owner and dispatcher.
         assertThat(childOwner).isNotEqualTo(parentOwner)
@@ -99,10 +99,8 @@
 
         // Verify that attempting to use the disposed dispatcher now throws an
         // IllegalStateException, preventing use-after-dispose bugs.
-        val inputHandler = DirectNavigationEventInputHandler()
-        assertThrows<IllegalStateException> {
-                childOwner.navigationEventDispatcher.addInputHandler(inputHandler)
-            }
+        val input = DirectNavigationEventInput()
+        assertThrows<IllegalStateException> { childOwner.navigationEventDispatcher.addInput(input) }
             .hasMessageThat()
             .contains("has already been disposed")
     }
@@ -133,9 +131,9 @@
 
         // Attempt to dispatch an event while the dispatcher is disabled.
         childOwner.navigationEventDispatcher.addCallback(callback)
-        val inputHandler = DirectNavigationEventInputHandler()
-        childOwner.navigationEventDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnCompleted()
+        val input = DirectNavigationEventInput()
+        childOwner.navigationEventDispatcher.addInput(input)
+        input.handleOnCompleted()
 
         assertThat(childOwner).isNotEqualTo(parentOwner)
         assertThat(childOwner.navigationEventDispatcher.isEnabled).isFalse()
@@ -164,9 +162,9 @@
 
         // Verify the root dispatcher can operate independently.
         rootOwner.navigationEventDispatcher.addCallback(callback)
-        val inputHandler = DirectNavigationEventInputHandler()
-        rootOwner.navigationEventDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnCompleted()
+        val input = DirectNavigationEventInput()
+        rootOwner.navigationEventDispatcher.addInput(input)
+        input.handleOnCompleted()
 
         assertThat(rootOwner.navigationEventDispatcher.isEnabled).isTrue()
 
@@ -198,10 +196,8 @@
 
         // Verify that using the disposed dispatcher throws the expected exception.
         // This prevents use-after-dispose bugs.
-        val inputHandler = DirectNavigationEventInputHandler()
-        assertThrows<IllegalStateException> {
-                rootOwner.navigationEventDispatcher.addInputHandler(inputHandler)
-            }
+        val input = DirectNavigationEventInput()
+        assertThrows<IllegalStateException> { rootOwner.navigationEventDispatcher.addInput(input) }
             .hasMessageThat()
             .contains("has already been disposed")
     }
@@ -227,9 +223,9 @@
 
         // Attempt to dispatch an event while disabled.
         rootOwner.navigationEventDispatcher.addCallback(callback)
-        val inputHandler = DirectNavigationEventInputHandler()
-        rootOwner.navigationEventDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnCompleted()
+        val input = DirectNavigationEventInput()
+        rootOwner.navigationEventDispatcher.addInput(input)
+        input.handleOnCompleted()
 
         assertThat(rootOwner.navigationEventDispatcher.isEnabled).isFalse()
 
diff --git a/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventHandlerTest.kt b/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventHandlerTest.kt
index cd5d435..19f833b6 100644
--- a/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventHandlerTest.kt
+++ b/navigationevent/navigationevent-compose/src/androidInstrumentedTest/kotlin/androidx/navigationevent/compose/NavigationEventHandlerTest.kt
@@ -25,7 +25,7 @@
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
 import androidx.kruth.assertThat
-import androidx.navigationevent.DirectNavigationEventInputHandler
+import androidx.navigationevent.DirectNavigationEventInput
 import androidx.navigationevent.NavigationEvent
 import androidx.navigationevent.testing.TestNavigationEventDispatcherOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -47,8 +47,7 @@
 
     private val owner = TestNavigationEventDispatcherOwner()
     private val dispatcher = owner.navigationEventDispatcher
-    private val inputHandler =
-        DirectNavigationEventInputHandler().also { dispatcher.addInputHandler(it) }
+    private val input = DirectNavigationEventInput().also { dispatcher.addInput(it) }
 
     @Test
     fun navigationEventHandler_whenOnStartDispatched_invokesHandler() {
@@ -61,7 +60,7 @@
                     progress.collect()
                 }
 
-                Button(onClick = { inputHandler.handleOnStarted(NavigationEvent()) }) {
+                Button(onClick = { input.handleOnStarted(NavigationEvent()) }) {
                     Text(text = "backPress")
                 }
             }
@@ -81,14 +80,14 @@
                     progress.collect()
                     counter++
                 }
-                Button(onClick = { inputHandler.handleOnStarted(NavigationEvent()) }) {
+                Button(onClick = { input.handleOnStarted(NavigationEvent()) }) {
                     Text(text = "backPress")
                 }
             }
         }
 
         rule.onNodeWithText("backPress").performClick()
-        inputHandler.handleOnCompleted()
+        input.handleOnCompleted()
 
         rule.runOnIdle { assertThat(counter).isEqualTo(1) }
     }
@@ -105,8 +104,8 @@
                 }
                 Button(
                     onClick = {
-                        inputHandler.handleOnStarted(NavigationEvent())
-                        inputHandler.handleOnCompleted()
+                        input.handleOnStarted(NavigationEvent())
+                        input.handleOnCompleted()
                     }
                 ) {
                     Text(text = "backPress")
@@ -140,8 +139,8 @@
 
         // Phase 1: Test when enabled
         // The handler should be called.
-        inputHandler.handleOnStarted(NavigationEvent())
-        inputHandler.handleOnCompleted()
+        input.handleOnStarted(NavigationEvent())
+        input.handleOnCompleted()
         rule.runOnIdle {
             assertThat(result).isEqualTo(listOf("onBack"))
             assertThat(owner.fallbackOnBackPressedInvocations).isEqualTo(0)
@@ -151,8 +150,8 @@
         // The fallback should be invoked instead of the handler.
         enabled = false
         rule.runOnIdle {
-            inputHandler.handleOnStarted(NavigationEvent())
-            inputHandler.handleOnCompleted()
+            input.handleOnStarted(NavigationEvent())
+            input.handleOnCompleted()
             assertThat(result).isEqualTo(listOf("onBack")) // Unchanged
             assertThat(owner.fallbackOnBackPressedInvocations).isEqualTo(1)
         }
@@ -161,8 +160,8 @@
         // The handler should work again.
         enabled = true
         rule.runOnIdle {
-            inputHandler.handleOnStarted(NavigationEvent())
-            inputHandler.handleOnCompleted()
+            input.handleOnStarted(NavigationEvent())
+            input.handleOnCompleted()
             assertThat(result).isEqualTo(listOf("onBack", "onBack"))
         }
     }
@@ -201,12 +200,12 @@
         // The 'enabled' check happens inside the callback. Disabling right before the
         // dispatch means the handler will start but see that it's disabled.
         count = 1
-        inputHandler.handleOnStarted(NavigationEvent())
+        input.handleOnStarted(NavigationEvent())
 
         // The launched effect for the handler might still run, but it should not prevent
         // the gesture from completing normally.
         rule.runOnIdle { assertThat(wasStartedWhenDisabled).isTrue() }
-        inputHandler.handleOnCompleted()
+        input.handleOnCompleted()
         rule.runOnIdle { assertThat(result).isEqualTo(listOf("onBack")) }
 
         // It should not be cancelled because the gesture completes successfully.
@@ -231,13 +230,13 @@
             }
         }
 
-        inputHandler.handleOnStarted(NavigationEvent())
+        input.handleOnStarted(NavigationEvent())
         // Disable after the gesture has already started. The `enabled` check has already passed,
         // so the gesture should continue to be handled.
         count = 1
 
         rule.runOnIdle { assertThat(wasStartedWhenDisabled).isFalse() }
-        inputHandler.handleOnCompleted()
+        input.handleOnCompleted()
         rule.runOnIdle { assertThat(result).isEqualTo(listOf("onBack")) }
     }
 
@@ -259,10 +258,10 @@
             }
         }
 
-        inputHandler.handleOnStarted(NavigationEvent())
+        input.handleOnStarted(NavigationEvent())
         // The exception is thrown on completion because the handler's coroutine finishes
         // prematurely without having suspended for the gesture's result.
-        inputHandler.handleOnCompleted()
+        input.handleOnCompleted()
 
         rule.waitUntil(1000) { result.size >= 3 }
         rule.runOnIdle { assertThat(result).isEqualTo(listOf("start", "async", "complete")) }
@@ -285,14 +284,14 @@
             }
         }
 
-        inputHandler.handleOnStarted(NavigationEvent())
-        inputHandler.handleOnCompleted()
+        input.handleOnStarted(NavigationEvent())
+        input.handleOnCompleted()
         rule.waitUntil { asyncStarted } // failing
 
         // Start a new gesture. This should cancel the scope of the previous handler,
         // including the async job it launched.
-        inputHandler.handleOnStarted(NavigationEvent())
-        inputHandler.handleOnCompleted()
+        input.handleOnStarted(NavigationEvent())
+        input.handleOnCompleted()
         rule.waitUntil(1000) { result.size >= 3 }
 
         rule.runOnIdle {
@@ -310,7 +309,7 @@
                     result += "parent"
                     progress.collect()
                 }
-                Button(onClick = { inputHandler.handleOnStarted(NavigationEvent()) }) {
+                Button(onClick = { input.handleOnStarted(NavigationEvent()) }) {
                     // When handlers are nested, only the deepest, last-composed handler is active.
                     NavigationEventHandler { progress ->
                         result += "child"
@@ -335,7 +334,7 @@
                     result += "parent"
                     progress.collect()
                 }
-                Button(onClick = { inputHandler.handleOnStarted(NavigationEvent()) }) {
+                Button(onClick = { input.handleOnStarted(NavigationEvent()) }) {
                     NavigationEventHandler(enabled = false) { progress ->
                         result += "child"
                         progress.collect()
@@ -363,7 +362,7 @@
                     result += "second"
                     progress.collect()
                 }
-                Button(onClick = { inputHandler.handleOnStarted(NavigationEvent()) }) {
+                Button(onClick = { input.handleOnStarted(NavigationEvent()) }) {
                     Text(text = "backPress")
                 }
             }
@@ -387,7 +386,7 @@
                     result += "second"
                     progress.collect()
                 }
-                Button(onClick = { inputHandler.handleOnStarted(NavigationEvent()) }) {
+                Button(onClick = { input.handleOnStarted(NavigationEvent()) }) {
                     Text(text = "backPress")
                 }
             }
@@ -411,7 +410,7 @@
                 // The key of the handler is its `onEvent` lambda. Changing it should
                 // correctly replace the old handler with the new one.
                 NavigationEventHandler(onEvent = handler)
-                Button(onClick = { inputHandler.handleOnStarted(NavigationEvent()) }) {
+                Button(onClick = { input.handleOnStarted(NavigationEvent()) }) {
                     Text(text = "backPress")
                 }
             }
@@ -441,10 +440,10 @@
             }
         }
 
-        inputHandler.handleOnStarted(NavigationEvent())
-        inputHandler.handleOnProgressed(NavigationEvent())
-        inputHandler.handleOnProgressed(NavigationEvent())
-        inputHandler.handleOnProgressed(NavigationEvent())
+        input.handleOnStarted(NavigationEvent())
+        input.handleOnProgressed(NavigationEvent())
+        input.handleOnProgressed(NavigationEvent())
+        input.handleOnProgressed(NavigationEvent())
 
         rule.waitForIdle()
         assertThat(result).isEqualTo(listOf(0, 1, 2))
@@ -470,9 +469,9 @@
             }
         }
 
-        inputHandler.handleOnStarted(NavigationEvent())
-        inputHandler.handleOnProgressed(NavigationEvent())
-        inputHandler.handleOnCancelled()
+        input.handleOnStarted(NavigationEvent())
+        input.handleOnProgressed(NavigationEvent())
+        input.handleOnCancelled()
 
         rule.runOnIdle {
             assertThat(result).isEqualTo(listOf("start", "progress", "navEvent cancelled"))
@@ -495,9 +494,9 @@
             }
         }
 
-        inputHandler.handleOnStarted(NavigationEvent())
-        inputHandler.handleOnProgressed(NavigationEvent())
-        inputHandler.handleOnCancelled()
+        input.handleOnStarted(NavigationEvent())
+        input.handleOnProgressed(NavigationEvent())
+        input.handleOnCancelled()
 
         rule.runOnIdle { assertThat(result).isEqualTo(listOf("start", "progress")) }
     }
@@ -520,9 +519,9 @@
         }
 
         // Simulate a cancelled gesture
-        inputHandler.handleOnStarted(NavigationEvent())
-        inputHandler.handleOnProgressed(NavigationEvent())
-        inputHandler.handleOnCancelled()
+        input.handleOnStarted(NavigationEvent())
+        input.handleOnProgressed(NavigationEvent())
+        input.handleOnCancelled()
 
         rule.runOnIdle {
             assertThat(result).isEqualTo(listOf("progress", "navEvent cancelled"))
@@ -530,10 +529,10 @@
         }
 
         // The handler should reset and be ready for a new gesture.
-        inputHandler.handleOnStarted(NavigationEvent())
-        inputHandler.handleOnProgressed(NavigationEvent())
-        inputHandler.handleOnProgressed(NavigationEvent())
-        inputHandler.handleOnCompleted()
+        input.handleOnStarted(NavigationEvent())
+        input.handleOnProgressed(NavigationEvent())
+        input.handleOnProgressed(NavigationEvent())
+        input.handleOnCompleted()
         rule.runOnIdle { assertThat(result).isEqualTo(listOf("progress", "progress", "complete")) }
     }
 
@@ -554,11 +553,11 @@
             }
         }
 
-        inputHandler.handleOnStarted(NavigationEvent())
+        input.handleOnStarted(NavigationEvent())
         rule.waitUntil { asyncStarted }
         // Cancelling the gesture should cancel the handler's scope, which in turn
         // cancels the async job.
-        inputHandler.handleOnCancelled()
+        input.handleOnCancelled()
 
         rule.runOnIdle {
             runBlocking { delay(700) } // allow time for the cancelled async job to not complete
@@ -589,7 +588,7 @@
         }
 
         // 1. Start the back gesture. The handler's coroutine is now running.
-        inputHandler.handleOnStarted(NavigationEvent())
+        input.handleOnStarted(NavigationEvent())
         rule.runOnIdle { assertThat(result).isEqualTo(listOf("start")) }
 
         // 2. Remove the handler from the composition.
@@ -600,7 +599,7 @@
 
         // 3. Attempt to complete the original gesture.
         // Since the handler was removed, it should not receive this event.
-        inputHandler.handleOnCompleted()
+        input.handleOnCompleted()
 
         // The handler should have been cancelled when it was disposed.
         // It should not have completed.
diff --git a/navigationevent/navigationevent-compose/src/commonMain/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwner.kt b/navigationevent/navigationevent-compose/src/commonMain/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwner.kt
index fa45ce8..359f4c0 100644
--- a/navigationevent/navigationevent-compose/src/commonMain/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwner.kt
+++ b/navigationevent/navigationevent-compose/src/commonMain/kotlin/androidx/navigationevent/compose/NavigationEventDispatcherOwner.kt
@@ -23,7 +23,7 @@
 import androidx.compose.runtime.remember
 import androidx.navigationevent.NavigationEventDispatcher
 import androidx.navigationevent.NavigationEventDispatcherOwner
-import androidx.navigationevent.NavigationEventInputHandler
+import androidx.navigationevent.NavigationEventInput
 
 /**
  * Creates a new navigation scope by providing a [NavigationEventDispatcher] to descendant
@@ -37,8 +37,8 @@
  * The dispatcher's lifecycle is automatically managed. It is created only once and automatically
  * disposed of when the composable leaves the composition, preventing memory leaks.
  *
- * When used to create a root dispatcher, you must use a [NavigationEventInputHandler] to send it
- * events. Otherwise, the dispatcher will be detached and will not receive events.
+ * When used to create a root dispatcher, you must use a [NavigationEventInput] to send it events.
+ * Otherwise, the dispatcher will be detached and will not receive events.
  *
  * **Null parent:** If [parent] is **EXPLICITLY** `null`, this creates a root dispatcher that runs
  * independently. By default, it requires a parent from the [LocalNavigationEventDispatcherOwner]
diff --git a/navigationevent/navigationevent-testing/build.gradle b/navigationevent/navigationevent-testing/build.gradle
index 4b216f9..e4d05a3 100644
--- a/navigationevent/navigationevent-testing/build.gradle
+++ b/navigationevent/navigationevent-testing/build.gradle
@@ -53,20 +53,6 @@
                 api(project(":navigationevent:navigationevent"))
             }
         }
-
-        wasmJsMain {
-            dependsOn(commonMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        jsMain {
-            dependsOn(commonMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
     }
 }
 
diff --git a/navigationevent/navigationevent/api/current.txt b/navigationevent/navigationevent/api/current.txt
index 25c7809..1570c8f 100644
--- a/navigationevent/navigationevent/api/current.txt
+++ b/navigationevent/navigationevent/api/current.txt
@@ -1,8 +1,8 @@
 // Signature format: 4.0
 package androidx.navigationevent {
 
-  public final class DirectNavigationEventInputHandler extends androidx.navigationevent.NavigationEventInputHandler {
-    ctor public DirectNavigationEventInputHandler();
+  public final class DirectNavigationEventInput extends androidx.navigationevent.NavigationEventInput {
+    ctor public DirectNavigationEventInput();
     method @MainThread public void handleOnCancelled();
     method @MainThread public void handleOnCompleted();
     method @MainThread public void handleOnProgressed(androidx.navigationevent.NavigationEvent event);
@@ -53,13 +53,13 @@
     ctor public NavigationEventDispatcher(optional kotlin.jvm.functions.Function0<kotlin.Unit>? fallbackOnBackPressed);
     method @KotlinOnly @MainThread public void addCallback(androidx.navigationevent.NavigationEventCallback<? extends java.lang.Object?> callback, optional androidx.navigationevent.NavigationEventPriority priority);
     method @BytecodeOnly @MainThread public void addCallback-3owFMvg(androidx.navigationevent.NavigationEventCallback<? extends java.lang.Object!>, int);
-    method @MainThread public void addInputHandler(androidx.navigationevent.NavigationEventInputHandler inputHandler);
+    method @MainThread public void addInput(androidx.navigationevent.NavigationEventInput input);
     method @MainThread public void dispose();
     method public kotlinx.coroutines.flow.StateFlow<androidx.navigationevent.NavigationEventState<androidx.navigationevent.NavigationEventInfo>> getState();
     method @KotlinOnly public inline <reified T extends androidx.navigationevent.NavigationEventInfo> kotlinx.coroutines.flow.StateFlow<androidx.navigationevent.NavigationEventState<T>> getState(kotlinx.coroutines.CoroutineScope scope, T initialInfo);
     method public boolean hasEnabledCallbacks();
     method public boolean isEnabled();
-    method @MainThread public void removeInputHandler(androidx.navigationevent.NavigationEventInputHandler inputHandler);
+    method @MainThread public void removeInput(androidx.navigationevent.NavigationEventInput input);
     method public void setEnabled(boolean);
     property public boolean isEnabled;
     property public kotlinx.coroutines.flow.StateFlow<androidx.navigationevent.NavigationEventState<androidx.navigationevent.NavigationEventInfo>> state;
@@ -77,15 +77,15 @@
     field public static final androidx.navigationevent.NavigationEventInfo.NotProvided INSTANCE;
   }
 
-  public abstract class NavigationEventInputHandler {
-    ctor public NavigationEventInputHandler();
+  public abstract class NavigationEventInput {
+    ctor public NavigationEventInput();
     method @MainThread protected final void dispatchOnCancelled();
     method @MainThread protected final void dispatchOnCompleted();
     method @MainThread protected final void dispatchOnProgressed(androidx.navigationevent.NavigationEvent event);
     method @MainThread protected final void dispatchOnStarted(androidx.navigationevent.NavigationEvent event);
-    method @EmptySuper @MainThread protected void onAttach(androidx.navigationevent.NavigationEventDispatcher dispatcher);
-    method @EmptySuper @MainThread protected void onDetach();
+    method @EmptySuper @MainThread protected void onAdded(androidx.navigationevent.NavigationEventDispatcher dispatcher);
     method @EmptySuper @MainThread protected void onHasEnabledCallbacksChanged(boolean hasEnabledCallbacks);
+    method @EmptySuper @MainThread protected void onRemoved();
   }
 
   @kotlin.jvm.JvmInline public final value class NavigationEventPriority {
@@ -122,8 +122,8 @@
     property public T? previousInfo;
   }
 
-  @RequiresApi(33) public final class OnBackInvokedInputHandler extends androidx.navigationevent.NavigationEventInputHandler {
-    ctor public OnBackInvokedInputHandler(android.window.OnBackInvokedDispatcher onBackInvokedDispatcher);
+  @RequiresApi(33) public final class OnBackInvokedInput extends androidx.navigationevent.NavigationEventInput {
+    ctor public OnBackInvokedInput(android.window.OnBackInvokedDispatcher onBackInvokedDispatcher);
   }
 
   public final class ViewTreeNavigationEventDispatcherOwner_androidKt {
diff --git a/navigationevent/navigationevent/api/restricted_current.txt b/navigationevent/navigationevent/api/restricted_current.txt
index 9054dba..eb72518 100644
--- a/navigationevent/navigationevent/api/restricted_current.txt
+++ b/navigationevent/navigationevent/api/restricted_current.txt
@@ -1,8 +1,8 @@
 // Signature format: 4.0
 package androidx.navigationevent {
 
-  public final class DirectNavigationEventInputHandler extends androidx.navigationevent.NavigationEventInputHandler {
-    ctor public DirectNavigationEventInputHandler();
+  public final class DirectNavigationEventInput extends androidx.navigationevent.NavigationEventInput {
+    ctor public DirectNavigationEventInput();
     method @MainThread public void handleOnCancelled();
     method @MainThread public void handleOnCompleted();
     method @MainThread public void handleOnProgressed(androidx.navigationevent.NavigationEvent event);
@@ -53,13 +53,13 @@
     ctor public NavigationEventDispatcher(optional kotlin.jvm.functions.Function0<kotlin.Unit>? fallbackOnBackPressed);
     method @KotlinOnly @MainThread public void addCallback(androidx.navigationevent.NavigationEventCallback<? extends java.lang.Object?> callback, optional androidx.navigationevent.NavigationEventPriority priority);
     method @BytecodeOnly @MainThread public void addCallback-3owFMvg(androidx.navigationevent.NavigationEventCallback<? extends java.lang.Object!>, int);
-    method @MainThread public void addInputHandler(androidx.navigationevent.NavigationEventInputHandler inputHandler);
+    method @MainThread public void addInput(androidx.navigationevent.NavigationEventInput input);
     method @MainThread public void dispose();
     method public kotlinx.coroutines.flow.StateFlow<androidx.navigationevent.NavigationEventState<androidx.navigationevent.NavigationEventInfo>> getState();
     method @KotlinOnly public inline <reified T extends androidx.navigationevent.NavigationEventInfo> kotlinx.coroutines.flow.StateFlow<androidx.navigationevent.NavigationEventState<T>> getState(kotlinx.coroutines.CoroutineScope scope, T initialInfo);
     method public boolean hasEnabledCallbacks();
     method public boolean isEnabled();
-    method @MainThread public void removeInputHandler(androidx.navigationevent.NavigationEventInputHandler inputHandler);
+    method @MainThread public void removeInput(androidx.navigationevent.NavigationEventInput input);
     method public void setEnabled(boolean);
     property public boolean isEnabled;
     property public kotlinx.coroutines.flow.StateFlow<androidx.navigationevent.NavigationEventState<androidx.navigationevent.NavigationEventInfo>> state;
@@ -77,15 +77,15 @@
     field public static final androidx.navigationevent.NavigationEventInfo.NotProvided INSTANCE;
   }
 
-  public abstract class NavigationEventInputHandler {
-    ctor public NavigationEventInputHandler();
+  public abstract class NavigationEventInput {
+    ctor public NavigationEventInput();
     method @MainThread protected final void dispatchOnCancelled();
     method @MainThread protected final void dispatchOnCompleted();
     method @MainThread protected final void dispatchOnProgressed(androidx.navigationevent.NavigationEvent event);
     method @MainThread protected final void dispatchOnStarted(androidx.navigationevent.NavigationEvent event);
-    method @EmptySuper @MainThread protected void onAttach(androidx.navigationevent.NavigationEventDispatcher dispatcher);
-    method @EmptySuper @MainThread protected void onDetach();
+    method @EmptySuper @MainThread protected void onAdded(androidx.navigationevent.NavigationEventDispatcher dispatcher);
     method @EmptySuper @MainThread protected void onHasEnabledCallbacksChanged(boolean hasEnabledCallbacks);
+    method @EmptySuper @MainThread protected void onRemoved();
   }
 
   @kotlin.jvm.JvmInline public final value class NavigationEventPriority {
@@ -123,8 +123,8 @@
     property public T? previousInfo;
   }
 
-  @RequiresApi(33) public final class OnBackInvokedInputHandler extends androidx.navigationevent.NavigationEventInputHandler {
-    ctor public OnBackInvokedInputHandler(android.window.OnBackInvokedDispatcher onBackInvokedDispatcher);
+  @RequiresApi(33) public final class OnBackInvokedInput extends androidx.navigationevent.NavigationEventInput {
+    ctor public OnBackInvokedInput(android.window.OnBackInvokedDispatcher onBackInvokedDispatcher);
   }
 
   public final class ViewTreeNavigationEventDispatcherOwner_androidKt {
diff --git a/navigationevent/navigationevent/bcv/native/current.txt b/navigationevent/navigationevent/bcv/native/current.txt
index 10829fb..728ea81 100644
--- a/navigationevent/navigationevent/bcv/native/current.txt
+++ b/navigationevent/navigationevent/bcv/native/current.txt
@@ -30,25 +30,25 @@
     open fun onEventStarted(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/NavigationEventCallback.onEventStarted|onEventStarted(androidx.navigationevent.NavigationEvent){}[0]
 }
 
-abstract class androidx.navigationevent/NavigationEventInputHandler { // androidx.navigationevent/NavigationEventInputHandler|null[0]
-    constructor <init>() // androidx.navigationevent/NavigationEventInputHandler.<init>|<init>(){}[0]
+abstract class androidx.navigationevent/NavigationEventInput { // androidx.navigationevent/NavigationEventInput|null[0]
+    constructor <init>() // androidx.navigationevent/NavigationEventInput.<init>|<init>(){}[0]
 
-    final fun dispatchOnCancelled() // androidx.navigationevent/NavigationEventInputHandler.dispatchOnCancelled|dispatchOnCancelled(){}[0]
-    final fun dispatchOnCompleted() // androidx.navigationevent/NavigationEventInputHandler.dispatchOnCompleted|dispatchOnCompleted(){}[0]
-    final fun dispatchOnProgressed(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/NavigationEventInputHandler.dispatchOnProgressed|dispatchOnProgressed(androidx.navigationevent.NavigationEvent){}[0]
-    final fun dispatchOnStarted(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/NavigationEventInputHandler.dispatchOnStarted|dispatchOnStarted(androidx.navigationevent.NavigationEvent){}[0]
-    open fun onAttach(androidx.navigationevent/NavigationEventDispatcher) // androidx.navigationevent/NavigationEventInputHandler.onAttach|onAttach(androidx.navigationevent.NavigationEventDispatcher){}[0]
-    open fun onDetach() // androidx.navigationevent/NavigationEventInputHandler.onDetach|onDetach(){}[0]
-    open fun onHasEnabledCallbacksChanged(kotlin/Boolean) // androidx.navigationevent/NavigationEventInputHandler.onHasEnabledCallbacksChanged|onHasEnabledCallbacksChanged(kotlin.Boolean){}[0]
+    final fun dispatchOnCancelled() // androidx.navigationevent/NavigationEventInput.dispatchOnCancelled|dispatchOnCancelled(){}[0]
+    final fun dispatchOnCompleted() // androidx.navigationevent/NavigationEventInput.dispatchOnCompleted|dispatchOnCompleted(){}[0]
+    final fun dispatchOnProgressed(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/NavigationEventInput.dispatchOnProgressed|dispatchOnProgressed(androidx.navigationevent.NavigationEvent){}[0]
+    final fun dispatchOnStarted(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/NavigationEventInput.dispatchOnStarted|dispatchOnStarted(androidx.navigationevent.NavigationEvent){}[0]
+    open fun onAdded(androidx.navigationevent/NavigationEventDispatcher) // androidx.navigationevent/NavigationEventInput.onAdded|onAdded(androidx.navigationevent.NavigationEventDispatcher){}[0]
+    open fun onHasEnabledCallbacksChanged(kotlin/Boolean) // androidx.navigationevent/NavigationEventInput.onHasEnabledCallbacksChanged|onHasEnabledCallbacksChanged(kotlin.Boolean){}[0]
+    open fun onRemoved() // androidx.navigationevent/NavigationEventInput.onRemoved|onRemoved(){}[0]
 }
 
-final class androidx.navigationevent/DirectNavigationEventInputHandler : androidx.navigationevent/NavigationEventInputHandler { // androidx.navigationevent/DirectNavigationEventInputHandler|null[0]
-    constructor <init>() // androidx.navigationevent/DirectNavigationEventInputHandler.<init>|<init>(){}[0]
+final class androidx.navigationevent/DirectNavigationEventInput : androidx.navigationevent/NavigationEventInput { // androidx.navigationevent/DirectNavigationEventInput|null[0]
+    constructor <init>() // androidx.navigationevent/DirectNavigationEventInput.<init>|<init>(){}[0]
 
-    final fun handleOnCancelled() // androidx.navigationevent/DirectNavigationEventInputHandler.handleOnCancelled|handleOnCancelled(){}[0]
-    final fun handleOnCompleted() // androidx.navigationevent/DirectNavigationEventInputHandler.handleOnCompleted|handleOnCompleted(){}[0]
-    final fun handleOnProgressed(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/DirectNavigationEventInputHandler.handleOnProgressed|handleOnProgressed(androidx.navigationevent.NavigationEvent){}[0]
-    final fun handleOnStarted(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/DirectNavigationEventInputHandler.handleOnStarted|handleOnStarted(androidx.navigationevent.NavigationEvent){}[0]
+    final fun handleOnCancelled() // androidx.navigationevent/DirectNavigationEventInput.handleOnCancelled|handleOnCancelled(){}[0]
+    final fun handleOnCompleted() // androidx.navigationevent/DirectNavigationEventInput.handleOnCompleted|handleOnCompleted(){}[0]
+    final fun handleOnProgressed(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/DirectNavigationEventInput.handleOnProgressed|handleOnProgressed(androidx.navigationevent.NavigationEvent){}[0]
+    final fun handleOnStarted(androidx.navigationevent/NavigationEvent) // androidx.navigationevent/DirectNavigationEventInput.handleOnStarted|handleOnStarted(androidx.navigationevent.NavigationEvent){}[0]
 }
 
 final class androidx.navigationevent/NavigationEvent { // androidx.navigationevent/NavigationEvent|null[0]
@@ -95,10 +95,10 @@
         final fun <set-isEnabled>(kotlin/Boolean) // androidx.navigationevent/NavigationEventDispatcher.isEnabled.<set-isEnabled>|<set-isEnabled>(kotlin.Boolean){}[0]
 
     final fun addCallback(androidx.navigationevent/NavigationEventCallback<*>, androidx.navigationevent/NavigationEventPriority = ...) // androidx.navigationevent/NavigationEventDispatcher.addCallback|addCallback(androidx.navigationevent.NavigationEventCallback<*>;androidx.navigationevent.NavigationEventPriority){}[0]
-    final fun addInputHandler(androidx.navigationevent/NavigationEventInputHandler) // androidx.navigationevent/NavigationEventDispatcher.addInputHandler|addInputHandler(androidx.navigationevent.NavigationEventInputHandler){}[0]
+    final fun addInput(androidx.navigationevent/NavigationEventInput) // androidx.navigationevent/NavigationEventDispatcher.addInput|addInput(androidx.navigationevent.NavigationEventInput){}[0]
     final fun dispose() // androidx.navigationevent/NavigationEventDispatcher.dispose|dispose(){}[0]
     final fun hasEnabledCallbacks(): kotlin/Boolean // androidx.navigationevent/NavigationEventDispatcher.hasEnabledCallbacks|hasEnabledCallbacks(){}[0]
-    final fun removeInputHandler(androidx.navigationevent/NavigationEventInputHandler) // androidx.navigationevent/NavigationEventDispatcher.removeInputHandler|removeInputHandler(androidx.navigationevent.NavigationEventInputHandler){}[0]
+    final fun removeInput(androidx.navigationevent/NavigationEventInput) // androidx.navigationevent/NavigationEventDispatcher.removeInput|removeInput(androidx.navigationevent.NavigationEventInput){}[0]
     final inline fun <#A1: reified androidx.navigationevent/NavigationEventInfo> getState(kotlinx.coroutines/CoroutineScope, #A1): kotlinx.coroutines.flow/StateFlow<androidx.navigationevent/NavigationEventState<#A1>> // androidx.navigationevent/NavigationEventDispatcher.getState|getState(kotlinx.coroutines.CoroutineScope;0:0){0§<androidx.navigationevent.NavigationEventInfo>}[0]
 }
 
diff --git a/navigationevent/navigationevent/build.gradle b/navigationevent/navigationevent/build.gradle
index 0eb1604..1c2ad5a 100644
--- a/navigationevent/navigationevent/build.gradle
+++ b/navigationevent/navigationevent/build.gradle
@@ -24,6 +24,8 @@
 
 import androidx.build.PlatformIdentifier
 import androidx.build.SoftwareType
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
+import org.jetbrains.kotlin.konan.target.Family
 
 plugins {
     id("AndroidXPlugin")
@@ -171,50 +173,27 @@
         }
 
         wasmJsMain {
-            dependsOn(webMain)
             dependencies {
-                implementation(libs.kotlinStdlibWasm)
                 implementation(libs.kotlinXw3c)
             }
         }
 
-        wasmJsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinTestForWasmTests)
-            }
-        }
-
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        jsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinTestJs)
-            }
-        }
-
         targets.configureEach { target ->
-            if (target.platformType == org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.native) {
+            if (target.platformType == KotlinPlatformType.native) {
                 if (target.konanTarget.family.appleFamily) {
                     target.compilations["main"].defaultSourceSet.dependsOn(darwinMain)
                     target.compilations["test"].defaultSourceSet.dependsOn(darwinTest)
-                } else if (target.konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX) {
+                } else if (target.konanTarget.family == Family.LINUX) {
                     target.compilations["main"].defaultSourceSet.dependsOn(linuxMain)
                     target.compilations["test"].defaultSourceSet.dependsOn(linuxTest)
-                } else if (target.konanTarget.family == org.jetbrains.kotlin.konan.target.Family.MINGW) {
+                } else if (target.konanTarget.family == Family.MINGW) {
                     target.compilations["main"].defaultSourceSet.dependsOn(mingwMain)
                     target.compilations["test"].defaultSourceSet.dependsOn(mingwTest)
                 } else {
                     target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
                     target.compilations["test"].defaultSourceSet.dependsOn(nativeTest)
                 }
-            } else if (target.platformType in [org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.js, org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.wasm]) {
+            } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
                 target.compilations["main"].defaultSourceSet.dependsOn(webMain)
                 target.compilations["test"].defaultSourceSet.dependsOn(webTest)
             }
diff --git a/navigationevent/navigationevent/src/androidInstrumentedTest/kotlin/androidx/navigationevent/OnBackInvokedInputHandlerTest.kt b/navigationevent/navigationevent/src/androidInstrumentedTest/kotlin/androidx/navigationevent/OnBackInvokedInputTest.kt
similarity index 85%
rename from navigationevent/navigationevent/src/androidInstrumentedTest/kotlin/androidx/navigationevent/OnBackInvokedInputHandlerTest.kt
rename to navigationevent/navigationevent/src/androidInstrumentedTest/kotlin/androidx/navigationevent/OnBackInvokedInputTest.kt
index b31d0c1..eca9cd6 100644
--- a/navigationevent/navigationevent/src/androidInstrumentedTest/kotlin/androidx/navigationevent/OnBackInvokedInputHandlerTest.kt
+++ b/navigationevent/navigationevent/src/androidInstrumentedTest/kotlin/androidx/navigationevent/OnBackInvokedInputTest.kt
@@ -27,15 +27,15 @@
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = 33)
-class OnBackInvokedInputHandlerTest {
+class OnBackInvokedInputTest {
 
     @Test
     fun testSimpleInvoker() {
         val invoker = TestOnBackInvokedDispatcher()
 
         val dispatcher = NavigationEventDispatcher {}
-        val inputHandler = OnBackInvokedInputHandler(invoker)
-        dispatcher.addInputHandler(inputHandler)
+        val input = OnBackInvokedInput(invoker)
+        dispatcher.addInput(input)
 
         val callback = TestNavigationEventCallback()
 
@@ -54,8 +54,8 @@
 
         val dispatcher = NavigationEventDispatcher {}
 
-        val inputHandler = OnBackInvokedInputHandler(invoker)
-        dispatcher.addInputHandler(inputHandler)
+        val input = OnBackInvokedInput(invoker)
+        dispatcher.addInput(input)
 
         val callback = TestNavigationEventCallback()
 
@@ -92,8 +92,8 @@
 
         val callback = TestNavigationEventCallback(isEnabled = false)
 
-        val inputHandler = OnBackInvokedInputHandler(invoker)
-        dispatcher.addInputHandler(inputHandler)
+        val input = OnBackInvokedInput(invoker)
+        dispatcher.addInput(input)
 
         dispatcher.addCallback(callback)
 
@@ -118,8 +118,8 @@
 
         dispatcher.addCallback(callback)
 
-        val inputHandler = OnBackInvokedInputHandler(invoker)
-        dispatcher.addInputHandler(inputHandler)
+        val input = OnBackInvokedInput(invoker)
+        dispatcher.addInput(input)
 
         assertThat(invoker.registerCount).isEqualTo(1)
 
@@ -135,8 +135,8 @@
 
         val dispatcher = NavigationEventDispatcher {}
 
-        val inputHandler = OnBackInvokedInputHandler(invoker)
-        dispatcher.addInputHandler(inputHandler)
+        val input = OnBackInvokedInput(invoker)
+        dispatcher.addInput(input)
 
         val callback = TestNavigationEventCallback()
 
@@ -165,8 +165,8 @@
 
         val dispatcher = NavigationEventDispatcher {}
 
-        val inputHandler = OnBackInvokedInputHandler(invoker)
-        dispatcher.addInputHandler(inputHandler)
+        val input = OnBackInvokedInput(invoker)
+        dispatcher.addInput(input)
 
         val callback = TestNavigationEventCallback()
 
@@ -189,8 +189,8 @@
 
         val dispatcher = NavigationEventDispatcher {}
 
-        val inputHandler = OnBackInvokedInputHandler(invoker)
-        dispatcher.addInputHandler(inputHandler)
+        val input = OnBackInvokedInput(invoker)
+        dispatcher.addInput(input)
 
         val callback = TestNavigationEventCallback(onEventStarted = { remove() })
 
@@ -211,8 +211,8 @@
         val invoker = TestOnBackInvokedDispatcher()
         val dispatcher = NavigationEventDispatcher {}
 
-        val inputHandler = OnBackInvokedInputHandler(invoker)
-        dispatcher.addInputHandler(inputHandler)
+        val input = OnBackInvokedInput(invoker)
+        dispatcher.addInput(input)
 
         val callback = TestNavigationEventCallback()
 
@@ -235,8 +235,8 @@
         val invoker = TestOnBackInvokedDispatcher()
 
         val dispatcher = NavigationEventDispatcher {}
-        val inputHandler = OnBackInvokedInputHandler(invoker)
-        dispatcher.addInputHandler(inputHandler)
+        val input = OnBackInvokedInput(invoker)
+        dispatcher.addInput(input)
 
         val callback1 = TestNavigationEventCallback()
 
diff --git a/navigationevent/navigationevent/src/androidMain/kotlin/androidx/navigationevent/OnBackInvokedInputHandler.android.kt b/navigationevent/navigationevent/src/androidMain/kotlin/androidx/navigationevent/OnBackInvokedInputHandler.android.kt
index 1456757..740cd81 100644
--- a/navigationevent/navigationevent/src/androidMain/kotlin/androidx/navigationevent/OnBackInvokedInputHandler.android.kt
+++ b/navigationevent/navigationevent/src/androidMain/kotlin/androidx/navigationevent/OnBackInvokedInputHandler.android.kt
@@ -25,9 +25,8 @@
 
 /** Provides input from OnBackInvokedCallback to the given [NavigationEventDispatcher]. */
 @RequiresApi(33)
-public class OnBackInvokedInputHandler(
-    private val onBackInvokedDispatcher: OnBackInvokedDispatcher
-) : NavigationEventInputHandler() {
+public class OnBackInvokedInput(private val onBackInvokedDispatcher: OnBackInvokedDispatcher) :
+    NavigationEventInput() {
     private val onBackInvokedCallback: OnBackInvokedCallback =
         if (Build.VERSION.SDK_INT == 33) {
             OnBackInvokedCallback { dispatchOnCompleted() }
@@ -37,11 +36,11 @@
 
     private var backInvokedCallbackRegistered = false
 
-    override fun onAttach(dispatcher: NavigationEventDispatcher) {
+    override fun onAdded(dispatcher: NavigationEventDispatcher) {
         updateBackInvokedCallbackState(dispatcher.hasEnabledCallbacks())
     }
 
-    override fun onDetach() {
+    override fun onRemoved() {
         updateBackInvokedCallbackState(false)
     }
 
diff --git a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/DirectNavigationEventInputHandler.kt b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/DirectNavigationEventInput.kt
similarity index 80%
rename from navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/DirectNavigationEventInputHandler.kt
rename to navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/DirectNavigationEventInput.kt
index 610f1ce..521759b 100644
--- a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/DirectNavigationEventInputHandler.kt
+++ b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/DirectNavigationEventInput.kt
@@ -18,8 +18,11 @@
 
 import androidx.annotation.MainThread
 
-/** An input handler that can send events to a [NavigationEventDispatcher]. */
-public class DirectNavigationEventInputHandler() : NavigationEventInputHandler() {
+/**
+ * An input that can send events to a [NavigationEventDispatcher]. Instead of subclassing
+ * [NavigationEventInput], users can create instances of this class and use it directly.
+ */
+public class DirectNavigationEventInput() : NavigationEventInput() {
     @MainThread
     public fun handleOnStarted(event: NavigationEvent) {
         dispatchOnStarted(event)
diff --git a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventCallback.kt b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventCallback.kt
index a2eb072..b899fb4 100644
--- a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventCallback.kt
+++ b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventCallback.kt
@@ -27,7 +27,7 @@
  *
  * @param isEnabled The initial enabled state for this callback. Defaults to `true`.
  * @see NavigationEventDispatcher
- * @see NavigationEventInputHandler
+ * @see NavigationEventInput
  */
 public abstract class NavigationEventCallback<T : NavigationEventInfo>(isEnabled: Boolean = true) {
 
@@ -106,7 +106,7 @@
      * @see onEventStarted
      * @see NavigationEventDispatcher.dispatchOnStarted
      */
-    internal fun doEventStarted(event: NavigationEvent) {
+    internal fun doOnEventStarted(event: NavigationEvent) {
         onEventStarted(event)
     }
 
@@ -126,7 +126,7 @@
      * @see onEventProgressed
      * @see NavigationEventDispatcher.dispatchOnProgressed
      */
-    internal fun doEventProgressed(event: NavigationEvent) {
+    internal fun doOnEventProgressed(event: NavigationEvent) {
         onEventProgressed(event)
     }
 
@@ -146,7 +146,7 @@
      * @see onEventCompleted
      * @see NavigationEventDispatcher.dispatchOnCompleted
      */
-    internal fun doEventCompleted() {
+    internal fun doOnEventCompleted() {
         onEventCompleted()
     }
 
@@ -164,7 +164,7 @@
      * @see onEventCancelled
      * @see NavigationEventDispatcher.dispatchOnCancelled
      */
-    internal fun doEventCancelled() {
+    internal fun doOnEventCancelled() {
         onEventCancelled()
     }
 
diff --git a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventDispatcher.kt b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventDispatcher.kt
index b9d4d88..87bdd06 100644
--- a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventDispatcher.kt
+++ b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventDispatcher.kt
@@ -173,16 +173,14 @@
     private val callbacks = mutableSetOf<NavigationEventCallback<*>>()
 
     /**
-     * A set of [NavigationEventInputHandler] instances that are directly managed by this
-     * dispatcher.
+     * A set of [NavigationEventInput] instances that are directly managed by this dispatcher.
      *
      * This dispatcher controls the lifecycle of its registered handlers, calling
-     * [NavigationEventInputHandler.doAttach] and [NavigationEventInputHandler.doDetach] as its own
-     * state changes.
+     * [NavigationEventInput.onAdded] and [NavigationEventInput.onRemoved] as its own state changes.
      *
      * **This is primarily for cleanup when this dispatcher is no longer needed.**
      */
-    private val inputs = mutableSetOf<NavigationEventInputHandler>()
+    private val inputs = mutableSetOf<NavigationEventInput>()
 
     /**
      * The [StateFlow] from the highest-priority, enabled navigation callback.
@@ -284,50 +282,56 @@
     }
 
     /**
-     * Adds an input handler, registering it with the shared processor and binding it to this
-     * dispatcher's lifecycle.
+     * Adds an input, registering it with the shared processor and binding it to this dispatcher's
+     * lifecycle.
      *
-     * The handler is registered globally with the [sharedProcessor] to receive system-wide state
+     * The input is registered globally with the [sharedProcessor] to receive system-wide state
      * updates (e.g., whether any callbacks are enabled). It is also tracked locally by this
      * dispatcher for lifecycle management.
      *
-     * The handler's [NavigationEventInputHandler.onAttach] method is invoked immediately upon
-     * addition. It will be automatically detached when this dispatcher [dispose] is called.
+     * The input's [NavigationEventInput.onAdded] method is invoked immediately upon addition. It
+     * will be automatically detached when this dispatcher [dispose] is called.
      *
-     * @param inputHandler The handler to add.
-     * @throws IllegalStateException if the dispatcher has already been disposed.
-     * @see removeInputHandler
-     * @see NavigationEventInputHandler.onDetach
+     * @param input The input to add.
+     * @throws IllegalStateException if the dispatcher has already been disposed or if [input] is
+     *   already added to a dispatcher.
+     * @see removeInput
+     * @see NavigationEventInput.onRemoved
      */
     @MainThread
-    public fun addInputHandler(inputHandler: NavigationEventInputHandler) {
+    public fun addInput(input: NavigationEventInput) {
         checkInvariants()
 
-        if (inputs.add(inputHandler)) {
-            sharedProcessor.inputs += inputHandler
-            inputHandler.doAttach(dispatcher = this)
+        if (inputs.add(input)) {
+            check(input.dispatcher == null) {
+                "This input is already added to dispatcher ${input.dispatcher}."
+            }
+            sharedProcessor.inputs += input
+            input.dispatcher = this
+            input.doOnAdded(this)
         }
     }
 
     /**
-     * Removes and detaches an input handler from this dispatcher and the shared processor.
+     * Removes and detaches an input from this dispatcher and the shared processor.
      *
-     * This severs the handler's lifecycle link to the dispatcher. Its
-     * [NavigationEventInputHandler.onDetach] method is invoked, and it will no longer receive
-     * lifecycle calls or global state updates from the processor.
+     * This severs the input's lifecycle link to the dispatcher. Its
+     * [NavigationEventInput.onRemoved] method is invoked, and it will no longer receive lifecycle
+     * calls or global state updates from the processor.
      *
-     * @param inputHandler The handler to remove.
+     * @param input The input to remove.
      * @throws IllegalStateException if the dispatcher has already been disposed.
-     * @see addInputHandler
-     * @see NavigationEventInputHandler.onAttach
+     * @see addInput
+     * @see NavigationEventInput.onAdded
      */
     @MainThread
-    public fun removeInputHandler(inputHandler: NavigationEventInputHandler) {
+    public fun removeInput(input: NavigationEventInput) {
         checkInvariants()
 
-        if (inputs.remove(inputHandler)) {
-            sharedProcessor.inputs -= inputHandler
-            inputHandler.doDetach()
+        if (inputs.remove(input)) {
+            sharedProcessor.inputs -= input
+            input.dispatcher = null
+            input.doOnRemoved()
         }
     }
 
@@ -335,80 +339,80 @@
      * Dispatch an [NavigationEventCallback.onEventStarted] event with the given event. This call is
      * delegated to the shared [NavigationEventProcessor].
      *
-     * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+     * @param input The [NavigationEventInput] that sourced this event.
      * @param direction The direction of the navigation event being started.
      * @param event [NavigationEvent] to dispatch to the callbacks.
      * @throws IllegalStateException if the dispatcher has already been disposed.
      */
     @MainThread
     internal fun dispatchOnStarted(
-        inputHandler: NavigationEventInputHandler,
+        input: NavigationEventInput,
         direction: NavigationEventDirection,
         event: NavigationEvent,
     ) {
         checkInvariants()
 
         if (!isEnabled) return
-        sharedProcessor.dispatchOnStarted(inputHandler, direction, event)
+        sharedProcessor.dispatchOnStarted(input, direction, event)
     }
 
     /**
      * Dispatch an [NavigationEventCallback.onEventProgressed] event with the given event. This call
      * is delegated to the shared [NavigationEventProcessor].
      *
-     * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+     * @param input The [NavigationEventInput] that sourced this event.
      * @param direction The direction of the navigation event being started.
      * @param event [NavigationEvent] to dispatch to the callbacks.
      * @throws IllegalStateException if the dispatcher has already been disposed.
      */
     @MainThread
     internal fun dispatchOnProgressed(
-        inputHandler: NavigationEventInputHandler,
+        input: NavigationEventInput,
         direction: NavigationEventDirection,
         event: NavigationEvent,
     ) {
         checkInvariants()
 
         if (!isEnabled) return
-        sharedProcessor.dispatchOnProgressed(inputHandler, direction, event)
+        sharedProcessor.dispatchOnProgressed(input, direction, event)
     }
 
     /**
      * Dispatch an [NavigationEventCallback.onEventCompleted] event. This call is delegated to the
      * shared [NavigationEventProcessor], passing the fallback action.
      *
-     * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+     * @param input The [NavigationEventInput] that sourced this event.
      * @param direction The direction of the navigation event being started.
      * @throws IllegalStateException if the dispatcher has already been disposed.
      */
     @MainThread
     internal fun dispatchOnCompleted(
-        inputHandler: NavigationEventInputHandler,
+        input: NavigationEventInput,
         direction: NavigationEventDirection,
     ) {
         checkInvariants()
 
         if (!isEnabled) return
-        sharedProcessor.dispatchOnCompleted(inputHandler, direction, fallbackOnBackPressed)
+        sharedProcessor.dispatchOnCompleted(input, direction, fallbackOnBackPressed)
     }
 
     /**
      * Dispatch an [NavigationEventCallback.onEventCancelled] event. This call is delegated to the
      * shared [NavigationEventProcessor].
      *
-     * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+     * @param input The [NavigationEventInput] that sourced this event.
      * @param direction The direction of the navigation event being started.
      * @throws IllegalStateException if the dispatcher has already been disposed.
      */
     @MainThread
     internal fun dispatchOnCancelled(
-        inputHandler: NavigationEventInputHandler,
+        input: NavigationEventInput,
         direction: NavigationEventDirection,
     ) {
         checkInvariants()
 
         if (!isEnabled) return
-        sharedProcessor.dispatchOnCancelled(inputHandler, direction)
+        sharedProcessor.dispatchOnCancelled(input, direction)
     }
 
     /**
@@ -422,9 +426,9 @@
      * Calling this method triggers a comprehensive, iterative cleanup:
      * 1. It iteratively processes and disposes of all child dispatchers and their descendants,
      *    ensuring a complete top-down cleanup of the entire sub-hierarchy without recursion.
-     * 2. For each dispatcher, it first detaches all registered [NavigationEventInputHandler]
-     *    instances by calling [NavigationEventInputHandler.onDetach]. This severs their lifecycle
-     *    link to the dispatcher and allows them to release any tied resources.
+     * 2. For each dispatcher, it first detaches all registered [NavigationEventInput] instances by
+     *    calling [NavigationEventInput.onRemoved]. This severs their lifecycle link to the
+     *    dispatcher and allows them to release any tied resources.
      * 3. It then removes all [NavigationEventCallback] instances registered with that dispatcher
      *    from the shared processor, preventing memory leaks.
      * 4. Finally, it removes the dispatcher from its parent's list of children, fully dismantling
@@ -452,12 +456,13 @@
             // own cleanup. This ensures a complete traversal of the sub-hierarchy.
             dispatchersToDispose += currentDispatcher.childDispatchers
 
-            // Notify all registered input handlers that this dispatcher is being disposed.
+            // Notify all registered inputs that this dispatcher is being disposed.
             // This gives them a chance to clean up their own state, severing the lifecycle link
             // and preventing them from interacting with a disposed object.
             for (input in currentDispatcher.inputs) {
                 sharedProcessor.inputs -= input
-                input.doDetach()
+                input.dispatcher = null
+                input.doOnRemoved()
             }
             inputs.clear()
 
diff --git a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventInput.kt b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventInput.kt
new file mode 100644
index 0000000..e3d6df2
--- /dev/null
+++ b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventInput.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.navigationevent
+
+import androidx.annotation.EmptySuper
+import androidx.annotation.MainThread
+import androidx.navigationevent.NavigationEventDirection.Companion.Backward
+
+/** A class that can send events to a [NavigationEventDispatcher]. */
+public abstract class NavigationEventInput() {
+
+    /** The [NavigationEventDispatcher] that this input is connected to. */
+    internal var dispatcher: NavigationEventDispatcher? = null
+
+    @MainThread
+    internal fun doOnAdded(dispatcher: NavigationEventDispatcher) {
+        onAdded(dispatcher)
+    }
+
+    @MainThread
+    internal fun doOnRemoved() {
+        onRemoved()
+    }
+
+    /**
+     * Called after this [NavigationEventInput] is added to [dispatcher]. This can happen when
+     * calling [NavigationEventDispatcher.addInput]. A [NavigationEventInput] can only be added to
+     * one [NavigationEventDispatcher] at a time.
+     *
+     * @param dispatcher The [NavigationEventDispatcher] that this input is now added to.
+     */
+    @MainThread @EmptySuper protected open fun onAdded(dispatcher: NavigationEventDispatcher) {}
+
+    /**
+     * Called after this [NavigationEventInput] is removed from a [NavigationEventDispatcher]. This
+     * can happen when calling [NavigationEventDispatcher.removeInput] or
+     * [NavigationEventDispatcher.dispose] on the containing [NavigationEventDispatcher].
+     */
+    @MainThread @EmptySuper protected open fun onRemoved() {}
+
+    @MainThread
+    internal fun doHasEnabledCallbacksChanged(hasEnabledCallbacks: Boolean) {
+        onHasEnabledCallbacksChanged(hasEnabledCallbacks)
+    }
+
+    /**
+     * Callback that will be notified when the connected dispatcher's `hasEnabledCallbacks` changes.
+     *
+     * @param hasEnabledCallbacks Whether the connected dispatcher has any enabled callbacks.
+     */
+    @MainThread
+    @EmptySuper
+    protected open fun onHasEnabledCallbacksChanged(hasEnabledCallbacks: Boolean) {}
+
+    /**
+     * Call `dispatchOnStarted` on the connected dispatcher.
+     *
+     * @param event The event to dispatch.
+     */
+    @MainThread
+    protected fun dispatchOnStarted(event: NavigationEvent) {
+        // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
+        dispatcher?.dispatchOnStarted(input = this, direction = Backward, event)
+            ?: error("This input is not added to any dispatcher.")
+    }
+
+    /**
+     * Call `dispatchOnProgressed` on the connected dispatcher.
+     *
+     * @param event The event to dispatch.
+     */
+    @MainThread
+    protected fun dispatchOnProgressed(event: NavigationEvent) {
+        // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
+        dispatcher?.dispatchOnProgressed(input = this, direction = Backward, event)
+            ?: error("This input is not added to any dispatcher.")
+    }
+
+    /** Call `dispatchOnCancelled` on the connected dispatcher. */
+    @MainThread
+    protected fun dispatchOnCancelled() {
+        // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
+        dispatcher?.dispatchOnCancelled(input = this, direction = Backward)
+            ?: error("This input is not added to any dispatcher.")
+    }
+
+    /** Call `dispatchOnCompleted` on the connected dispatcher. */
+    @MainThread
+    protected fun dispatchOnCompleted() {
+        // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
+        dispatcher?.dispatchOnCompleted(input = this, direction = Backward)
+            ?: error("This input is not added to any dispatcher.")
+    }
+}
diff --git a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventInputHandler.kt b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventInputHandler.kt
deleted file mode 100644
index 4cd2ffa..0000000
--- a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventInputHandler.kt
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Copyright 2025 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.navigationevent
-
-import androidx.annotation.EmptySuper
-import androidx.annotation.MainThread
-import androidx.navigationevent.NavigationEventDirection.Companion.Backward
-
-/** An input handler that can send events to a [NavigationEventDispatcher]. */
-public abstract class NavigationEventInputHandler() {
-
-    private var dispatcher: NavigationEventDispatcher? = null
-
-    /**
-     * Attaches this [NavigationEventInputHandler] to [dispatcher].
-     *
-     * @param dispatcher The [NavigationEventDispatcher] to attach to.
-     * @throws IllegalStateException if it's already attached to a dispatcher.
-     */
-    @MainThread
-    internal fun doAttach(dispatcher: NavigationEventDispatcher) {
-        check(this.dispatcher == null) {
-            "This input handler is already attached to dispatcher ${this.dispatcher}."
-        }
-        this.dispatcher = dispatcher
-
-        onAttach(dispatcher)
-    }
-
-    /**
-     * Detaches this [NavigationEventInputHandler] from the attached [NavigationEventDispatcher]. If
-     * it's not attached to a dispatcher, this function does nothing.
-     */
-    @MainThread
-    internal fun doDetach() {
-        if (this.dispatcher == null) return
-        this.dispatcher = null
-        onDetach()
-    }
-
-    /**
-     * Called after this [NavigationEventInputHandler] is attached to [dispatcher]. This can happen
-     * when calling [NavigationEventDispatcher.addInputHandler]. A [NavigationEventInputHandler] can
-     * only be attached to one [NavigationEventDispatcher] at a time.
-     *
-     * @param dispatcher The [NavigationEventDispatcher] that this input handler is now attached to.
-     */
-    @MainThread @EmptySuper protected open fun onAttach(dispatcher: NavigationEventDispatcher) {}
-
-    /**
-     * Called after this [NavigationEventInputHandler] is detached from a
-     * [NavigationEventDispatcher]. This can happen when calling
-     * [NavigationEventDispatcher.removeInputHandler] or [NavigationEventDispatcher.dispose] on the
-     * attached [NavigationEventDispatcher].
-     */
-    @MainThread @EmptySuper protected open fun onDetach() {}
-
-    @MainThread
-    internal fun doHasEnabledCallbacksChanged(hasEnabledCallbacks: Boolean) {
-        onHasEnabledCallbacksChanged(hasEnabledCallbacks)
-    }
-
-    /**
-     * Callback that will be notified when the connected dispatcher's `hasEnabledCallbacks` changes.
-     *
-     * @param hasEnabledCallbacks Whether the connected dispatcher has any enabled callbacks.
-     */
-    @MainThread
-    @EmptySuper
-    protected open fun onHasEnabledCallbacksChanged(hasEnabledCallbacks: Boolean) {}
-
-    /**
-     * Call `dispatchOnStarted` on the connected dispatcher.
-     *
-     * @param event The event to dispatch.
-     */
-    @MainThread
-    protected fun dispatchOnStarted(event: NavigationEvent) {
-        // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
-        dispatcher?.dispatchOnStarted(inputHandler = this, direction = Backward, event)
-            ?: error("This input handler is not attached to a dispatcher.")
-    }
-
-    /**
-     * Call `dispatchOnProgressed` on the connected dispatcher.
-     *
-     * @param event The event to dispatch.
-     */
-    @MainThread
-    protected fun dispatchOnProgressed(event: NavigationEvent) {
-        // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
-        dispatcher?.dispatchOnProgressed(inputHandler = this, direction = Backward, event)
-            ?: error("This input handler is not attached to a dispatcher.")
-    }
-
-    /** Call `dispatchOnCancelled` on the connected dispatcher. */
-    @MainThread
-    protected fun dispatchOnCancelled() {
-        // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
-        dispatcher?.dispatchOnCancelled(inputHandler = this, direction = Backward)
-            ?: error("This input handler is not attached to a dispatcher.")
-    }
-
-    /** Call `dispatchOnCompleted` on the connected dispatcher. */
-    @MainThread
-    protected fun dispatchOnCompleted() {
-        // TODO(kuanyingchou): Accept a direction parameter instead of hardcoding `Backward`.
-        dispatcher?.dispatchOnCompleted(inputHandler = this, direction = Backward)
-            ?: error("This input handler is not attached to a dispatcher.")
-    }
-}
diff --git a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventProcessor.kt b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventProcessor.kt
index 4d2569a..c9e6d7b 100644
--- a/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventProcessor.kt
+++ b/navigationevent/navigationevent/src/commonMain/kotlin/androidx/navigationevent/NavigationEventProcessor.kt
@@ -83,7 +83,7 @@
     private var inProgressCallback: NavigationEventCallback<*>? = null
 
     /**
-     * A central registry of all active [NavigationEventInputHandler] instances associated with this
+     * A central registry of all active [NavigationEventInput] instances associated with this
      * processor.
      *
      * This set is managed by the [NavigationEventDispatcher] and allows the processor to
@@ -92,7 +92,7 @@
      *
      * It is not intended for direct public use and is exposed internally for the dispatcher.
      */
-    val inputs = mutableSetOf<NavigationEventInputHandler>()
+    val inputs = mutableSetOf<NavigationEventInput>()
 
     /**
      * Represents whether there is at least one enabled callback registered across all dispatchers.
@@ -102,8 +102,7 @@
      * `OnBackPressedDispatcher.setEnabled()`.
      *
      * It is updated automatically when callbacks are added, removed, or their enabled state
-     * changes. When its value changes, it notifies all registered [NavigationEventInputHandler]
-     * instances.
+     * changes. When its value changes, it notifies all registered [NavigationEventInput] instances.
      */
     private var hasEnabledCallbacks: Boolean = false
         set(value) {
@@ -232,7 +231,7 @@
         // If the callback is the one currently being processed, it needs to be notified of
         // cancellation and then cleared from the in-progress state.
         if (callback == inProgressCallback) {
-            callback.doEventCancelled()
+            callback.doOnEventCancelled()
             inProgressCallback = null
         }
 
@@ -255,23 +254,23 @@
      * the new event. Only the single, highest-priority enabled callback is notified and becomes the
      * `inProgressCallback`.
      *
-     * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+     * @param input The [NavigationEventInput] that sourced this event.
      * @param direction The direction of the navigation event being started.
      * @param event [NavigationEvent] to dispatch to the callback.
      */
     @MainThread
     fun dispatchOnStarted(
-        inputHandler: NavigationEventInputHandler,
+        input: NavigationEventInput,
         direction: NavigationEventDirection,
         event: NavigationEvent,
     ) {
-        // TODO(mgalhardo): Update sharedProcessor to use the inputHandler to distinguish events.
+        // TODO(mgalhardo): Update sharedProcessor to use input to distinguish events.
         // TODO(mgalhardo): Update the sharedProcessor to handle NavigationEventDirection.
 
         if (inProgressCallback != null) {
             // It's important to ensure that any ongoing operations from previous events are
             // properly cancelled before starting new ones to maintain a consistent state.
-            dispatchOnCancelled(inputHandler, direction)
+            dispatchOnCancelled(input, direction)
         }
 
         // Find the highest-priority enabled callback to handle this event.
@@ -281,7 +280,7 @@
             // `onCancelled` can be correctly handled if the callback removes itself during
             // `onEventStarted`.
             inProgressCallback = callback
-            callback.doEventStarted(event)
+            callback.doOnEventStarted(event)
             _state.update {
                 InProgress(callback.currentInfo ?: NotProvided, callback.previousInfo, event)
             }
@@ -295,17 +294,17 @@
      * will be notified. Otherwise, the highest-priority enabled callback will receive the progress
      * event. This is not a terminal event, so `inProgressCallback` is not cleared.
      *
-     * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+     * @param input The [NavigationEventInput] that sourced this event.
      * @param direction The direction of the navigation event being started.
      * @param event [NavigationEvent] to dispatch to the callback.
      */
     @MainThread
     fun dispatchOnProgressed(
-        inputHandler: NavigationEventInputHandler,
+        input: NavigationEventInput,
         direction: NavigationEventDirection,
         event: NavigationEvent,
     ) {
-        // TODO(mgalhardo): Update sharedProcessor to use the inputHandler to distinguish events.
+        // TODO(mgalhardo): Update sharedProcessor to use input to distinguish events.
         // TODO(mgalhardo): Update the sharedProcessor to handle NavigationEventDirection.
 
         // If there is a callback in progress, only that one is notified.
@@ -314,7 +313,7 @@
         // Progressed is not a terminal event, so `inProgressCallback` is not cleared.
 
         if (callback != null) {
-            callback.doEventProgressed(event)
+            callback.doOnEventProgressed(event)
             _state.update {
                 InProgress(callback.currentInfo ?: NotProvided, callback.previousInfo, event)
             }
@@ -329,17 +328,17 @@
      * `inProgressCallback`. If no callback handles the event, the `fallbackOnBackPressed` action is
      * invoked.
      *
-     * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+     * @param input The [NavigationEventInput] that sourced this event.
      * @param direction The direction of the navigation event being started.
      * @param fallbackOnBackPressed The action to invoke if no callback handles the completion.
      */
     @MainThread
     fun dispatchOnCompleted(
-        inputHandler: NavigationEventInputHandler,
+        input: NavigationEventInput,
         direction: NavigationEventDirection,
         fallbackOnBackPressed: (() -> Unit)?,
     ) {
-        // TODO(mgalhardo): Update sharedProcessor to use the inputHandler to distinguish events.
+        // TODO(mgalhardo): Update sharedProcessor to use input to distinguish events.
         // TODO(mgalhardo): Update the sharedProcessor to handle NavigationEventDirection.
 
         // If there is a callback in progress, only that one is notified.
@@ -351,7 +350,7 @@
         if (callback == null) {
             fallbackOnBackPressed?.invoke()
         } else {
-            callback.doEventCompleted()
+            callback.doOnEventCompleted()
             _state.update { Idle(callback.currentInfo ?: NotProvided) }
         }
     }
@@ -363,15 +362,12 @@
      * highest-priority enabled callback will be notified. This is a terminal event, clearing the
      * `inProgressCallback`.
      *
-     * @param inputHandler The [NavigationEventInputHandler] that sourced this event.
+     * @param input The [NavigationEventInput] that sourced this event.
      * @param direction The direction of the navigation event being started.
      */
     @MainThread
-    fun dispatchOnCancelled(
-        inputHandler: NavigationEventInputHandler,
-        direction: NavigationEventDirection,
-    ) {
-        // TODO(mgalhardo): Update sharedProcessor to use the inputHandler to distinguish events.
+    fun dispatchOnCancelled(input: NavigationEventInput, direction: NavigationEventDirection) {
+        // TODO(mgalhardo): Update sharedProcessor to use input to distinguish events.
         // TODO(mgalhardo): Update the sharedProcessor to handle NavigationEventDirection.
 
         // If there is a callback in progress, only that one is notified.
@@ -380,7 +376,7 @@
         inProgressCallback = null // Clear in-progress, as 'cancelled' is a terminal event.
 
         if (callback != null) {
-            callback.doEventCancelled()
+            callback.doOnEventCancelled()
             _state.update { Idle(callback.currentInfo ?: NotProvided) }
         }
     }
diff --git a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventDispatcherStateTest.kt b/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventDispatcherStateTest.kt
deleted file mode 100644
index ceab18c..0000000
--- a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventDispatcherStateTest.kt
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
- * Copyright 2025 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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(ExperimentalCoroutinesApi::class)
-
-package androidx.navigationevent
-
-import androidx.kruth.assertThat
-import androidx.navigationevent.NavigationEventInfo.NotProvided
-import androidx.navigationevent.NavigationEventState.Idle
-import androidx.navigationevent.NavigationEventState.InProgress
-import androidx.navigationevent.testing.TestNavigationEventCallback
-import androidx.navigationevent.testing.TestNavigationEventDispatcherOwner
-import kotlin.test.Test
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.runTest
-
-class NavigationEventDispatcherStateTest {
-
-    private val dispatcherOwner = TestNavigationEventDispatcherOwner()
-    private val dispatcher = dispatcherOwner.navigationEventDispatcher
-    private val inputHandler =
-        DirectNavigationEventInputHandler().also { dispatcher.addInputHandler(it) }
-
-    @Test
-    fun state_whenMultipleCallbacksAreAdded_thenReflectsInfoFromLastAddedCallback() = runTest {
-        val homeCallback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("home"))
-        val detailsCallback =
-            TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
-
-        assertThat(dispatcher.state.value).isEqualTo(Idle(NotProvided))
-
-        dispatcher.addCallback(homeCallback)
-        assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("home")))
-
-        // Callbacks are prioritized like a stack (LIFO), so adding a new one makes it active.
-        dispatcher.addCallback(detailsCallback)
-        assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("details")))
-    }
-
-    @Test
-    fun state_whenSetInfoIsCalledOnActiveCallback_thenStateIsUpdated() = runTest {
-        val callback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("initial"))
-        dispatcher.addCallback(callback)
-
-        assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("initial")))
-
-        // Calling setInfo on the active callback should immediately update the dispatcher's state.
-        callback.setInfo(currentInfo = HomeScreenInfo("updated"), previousInfo = null)
-
-        assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("updated")))
-    }
-
-    @Test
-    fun state_whenSetInfoIsCalledOnInactiveCallback_thenStateIsUnchanged() = runTest {
-        val homeCallback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("home"))
-        val detailsCallback =
-            TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
-        dispatcher.addCallback(homeCallback)
-        dispatcher.addCallback(detailsCallback)
-
-        // The state should reflect the last-added (active) callback.
-        assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("details")))
-
-        // Calling setInfo on an inactive callback should NOT affect the global state.
-        // This confirms our logic in `onCallbackInfoChanged` is working correctly.
-        homeCallback.setInfo(currentInfo = HomeScreenInfo("home-updated"), previousInfo = null)
-
-        // The state should remain unchanged because the update came from a non-active callback.
-        assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("details")))
-    }
-
-    @Test
-    fun state_whenFullGestureLifecycleIsDispatched_thenTransitionsToInProgressAndBackToIdle() {
-        val callbackInfo = HomeScreenInfo("home")
-        val callback = TestNavigationEventCallback(currentInfo = callbackInfo)
-        dispatcher.addCallback(callback)
-
-        val startEvent = NavigationEvent(touchX = 0.1F)
-        val progressEvent = NavigationEvent(touchX = 0.3f)
-
-        assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
-
-        // Starting a gesture should move the state to InProgress with the start event.
-        inputHandler.handleOnStarted(startEvent)
-        var state = dispatcher.state.value as InProgress
-        assertThat(state.currentInfo).isEqualTo(callbackInfo)
-        assertThat(state.previousInfo).isNull()
-        assertThat(state.latestEvent).isEqualTo(startEvent)
-
-        // Progressing the gesture should keep it InProgress but update to the latest event.
-        inputHandler.handleOnProgressed(progressEvent)
-        state = dispatcher.state.value as InProgress
-        assertThat(state.latestEvent).isEqualTo(progressEvent)
-
-        // Completing the gesture should return the state to Idle.
-        inputHandler.handleOnCompleted()
-        assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
-    }
-
-    @Test
-    fun state_whenGestureIsCancelled_thenReturnsToIdleState() {
-        val callbackInfo = HomeScreenInfo("home")
-        val callback = TestNavigationEventCallback(currentInfo = callbackInfo)
-        dispatcher.addCallback(callback)
-
-        val startEvent = NavigationEvent()
-
-        assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
-
-        // Starting a gesture moves the state to InProgress.
-        inputHandler.handleOnStarted(startEvent)
-        assertThat(dispatcher.state.value).isEqualTo(InProgress(callbackInfo, null, startEvent))
-
-        // Cancelling the gesture should also return the state to Idle.
-        inputHandler.handleOnCancelled()
-        assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
-    }
-
-    @Test
-    fun inProgressState_whenInfoIsUpdatedDuringGesture_thenReflectsCorrectStateProperties() {
-        val firstInfo = HomeScreenInfo("initial")
-        val callback = TestNavigationEventCallback(currentInfo = firstInfo)
-        dispatcher.addCallback(callback)
-
-        val startEvent = NavigationEvent(touchX = 0.1F)
-
-        // Start the gesture.
-        inputHandler.handleOnStarted(startEvent)
-
-        // At the start, previousInfo is null.
-        var state = dispatcher.state.value as InProgress
-        assertThat(state.currentInfo).isEqualTo(firstInfo)
-        assertThat(state.previousInfo).isNull()
-        assertThat(state.latestEvent).isEqualTo(startEvent)
-
-        // Update the info mid-gesture. This triggers our updated `onCallbackInfoChanged` logic.
-        val secondInfo = HomeScreenInfo("updated")
-        callback.setInfo(currentInfo = secondInfo, previousInfo = firstInfo)
-
-        // The state should now reflect the updated info. The `previousInfo` is now captured.
-        state = dispatcher.state.value as InProgress
-        assertThat(state.currentInfo).isEqualTo(secondInfo)
-        assertThat(state.previousInfo).isEqualTo(firstInfo)
-        assertThat(state.latestEvent).isEqualTo(startEvent) // Event hasn't changed yet.
-
-        // Complete the gesture.
-        inputHandler.handleOnCompleted()
-        assertThat(dispatcher.state.value).isEqualTo(Idle(secondInfo))
-    }
-
-    @Test
-    fun inProgressState_whenNewGestureStartsAfterAnotherCompletes_thenPreviousInfoIsNotStale() {
-        val initialInfo = HomeScreenInfo("initial")
-        val callback = TestNavigationEventCallback(currentInfo = initialInfo)
-        dispatcher.addCallback(callback)
-
-        // FIRST GESTURE: Create a complex state.
-        inputHandler.handleOnStarted(NavigationEvent(touchX = 0.1f))
-        callback.setInfo(currentInfo = HomeScreenInfo("updated"), previousInfo = null)
-        inputHandler.handleOnCompleted()
-
-        // After the first gesture, the final state is Idle with the updated info.
-        val finalInfo = HomeScreenInfo("updated")
-        assertThat(dispatcher.state.value).isEqualTo(Idle(finalInfo))
-
-        // SECOND GESTURE: Verify that previousInfo was cleared by `clearPreviousInfo()`.
-        val event2 = NavigationEvent(touchX = 0.3f)
-        inputHandler.handleOnStarted(event2)
-
-        // When a new gesture starts, `previousInfo` should be null, not stale data.
-        val state = dispatcher.state.value as InProgress
-        assertThat(state.currentInfo).isEqualTo(finalInfo)
-        assertThat(state.previousInfo).isNull()
-        assertThat(state.latestEvent).isEqualTo(event2)
-    }
-
-    @Test
-    fun state_whenActiveDispatcherIsDisabled_fallsBackToSiblingDispatcherCallback() {
-        // Create two sibling dispatchers sharing the same owner and processor.
-        val childDispatcher = NavigationEventDispatcher(parentDispatcher = dispatcher)
-
-        val callbackA = TestNavigationEventCallback(currentInfo = HomeScreenInfo("A"))
-        dispatcher.addCallback(callbackA)
-        assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("A")))
-
-        val callbackB = TestNavigationEventCallback(currentInfo = DetailsScreenInfo("B"))
-        childDispatcher.addCallback(callbackB)
-        // Assert that state reflects callbackB, which was added last and is now active.
-        assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("B")))
-
-        // Disable the dispatcher that hosts the currently active callback.
-        childDispatcher.isEnabled = false
-
-        // The processor should now ignore callbackB and the state should
-        // fall back to the next-highest priority callback, which is callbackA.
-        assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("A")))
-
-        // Re-enable the dispatcher.
-        childDispatcher.isEnabled = true
-
-        // The state should once again reflect callbackB, as it's now enabled
-        // and has higher priority (due to being added last).
-        assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("B")))
-    }
-
-    @Test
-    fun getState_whenFilteredForSpecificType_onlyEmitsMatchingStates() =
-        runTest(UnconfinedTestDispatcher()) {
-            val initialHomeInfo = HomeScreenInfo("initial")
-            val homeCallback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("home"))
-            val detailsCallback =
-                TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
-            val collectedStates = mutableListOf<NavigationEventState<HomeScreenInfo>>()
-
-            dispatcher
-                .getState(backgroundScope, initialHomeInfo)
-                .onEach { collectedStates.add(it) }
-                .launchIn(backgroundScope)
-            advanceUntilIdle()
-
-            // The flow must start with the initial value provided.
-            assertThat(collectedStates).hasSize(1)
-            assertThat(collectedStates.last()).isEqualTo(Idle(initialHomeInfo))
-
-            // A new state with a matching type should be collected.
-            dispatcher.addCallback(homeCallback)
-            advanceUntilIdle()
-            assertThat(collectedStates).hasSize(2)
-            assertThat(collectedStates.last()).isEqualTo(Idle(HomeScreenInfo("home")))
-
-            // A state with a non-matching type should be filtered out and not collected.
-            dispatcher.addCallback(detailsCallback)
-            advanceUntilIdle()
-            assertThat(collectedStates).hasSize(2)
-
-            // When the active callback is removed, since a non-matching type should be filtered out
-            // and not collected.
-            detailsCallback.remove()
-            advanceUntilIdle()
-            assertThat(collectedStates).hasSize(2)
-            assertThat(collectedStates.last()).isEqualTo(Idle(HomeScreenInfo("home")))
-        }
-
-    @Test
-    fun getState_whenTypeDoesNotMatch_emitsOnlyInitialInfo() =
-        runTest(UnconfinedTestDispatcher()) {
-            val initialHomeInfo = HomeScreenInfo("initial")
-            val detailsCallback =
-                TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
-            val collectedStates = mutableListOf<NavigationEventState<HomeScreenInfo>>()
-
-            dispatcher
-                .getState(backgroundScope, initialHomeInfo)
-                .onEach { collectedStates.add(it) }
-                .launchIn(backgroundScope)
-            advanceUntilIdle()
-
-            // The flow must start with its initial value.
-            assertThat(collectedStates).hasSize(1)
-            assertThat(collectedStates.first()).isEqualTo(Idle(initialHomeInfo))
-
-            // Add a callback with a non-matching type.
-            dispatcher.addCallback(detailsCallback)
-            advanceUntilIdle()
-
-            // The collector should not have emitted a new value.
-            assertThat(collectedStates).hasSize(1)
-
-            // Update the non-matching callback's info.
-            detailsCallback.setInfo(
-                currentInfo = DetailsScreenInfo("details-updated"),
-                previousInfo = null,
-            )
-            advanceUntilIdle()
-
-            // The collector should still not have emitted a new value.
-            assertThat(collectedStates).hasSize(1)
-        }
-
-    @Test
-    fun progress_whenIdleOrInProgress_returnsCorrectValue() {
-        val callbackInfo = HomeScreenInfo("home")
-        val callback = TestNavigationEventCallback(currentInfo = callbackInfo)
-        dispatcher.addCallback(callback)
-
-        // Before any gesture, the state is Idle and progress should be 0.
-        assertThat(dispatcher.state.value.progress).isEqualTo(0f)
-
-        // Start a gesture.
-        inputHandler.handleOnStarted(NavigationEvent(progress = 0.1f))
-        assertThat(dispatcher.state.value.progress).isEqualTo(0.1f)
-
-        // InProgress state should reflect the event's progress.
-        inputHandler.handleOnProgressed(NavigationEvent(progress = 0.5f))
-        assertThat(dispatcher.state.value.progress).isEqualTo(0.5f)
-
-        // Complete the gesture.
-        inputHandler.handleOnCompleted()
-
-        // After the gesture, the state is Idle again and progress should be 0.
-        assertThat(dispatcher.state.value.progress).isEqualTo(0f)
-    }
-}
-
-/** A sealed interface for type-safe navigation information. */
-sealed interface TestInfo : NavigationEventInfo
-
-data class HomeScreenInfo(val id: String) : TestInfo
-
-data class DetailsScreenInfo(val id: String) : TestInfo
diff --git a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventDispatcherTest.kt b/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventDispatcherTest.kt
index 386ab96..83e4da8 100644
--- a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventDispatcherTest.kt
+++ b/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventDispatcherTest.kt
@@ -13,26 +13,38 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@file:OptIn(ExperimentalCoroutinesApi::class)
 
 package androidx.navigationevent
 
+import androidx.annotation.MainThread
 import androidx.kruth.assertThat
 import androidx.kruth.assertThrows
-import androidx.navigationevent.internal.TestNavigationEventInput
+import androidx.navigationevent.NavigationEventInfo.NotProvided
+import androidx.navigationevent.NavigationEventState.Idle
+import androidx.navigationevent.NavigationEventState.InProgress
 import androidx.navigationevent.testing.TestNavigationEventCallback
 import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
 
 class NavigationEventDispatcherTest {
 
+    // region Core API
+
     @Test
-    fun dispatch_onStarted_thenOnStartedIsSent() {
+    fun dispatch_onStarted_sendsEventToCallback() {
         val dispatcher = NavigationEventDispatcher()
         val callback = TestNavigationEventCallback()
         dispatcher.addCallback(callback)
 
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(NavigationEvent())
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnStarted(NavigationEvent())
 
         assertThat(callback.startedInvocations).isEqualTo(1)
         assertThat(callback.progressedInvocations).isEqualTo(0)
@@ -41,14 +53,14 @@
     }
 
     @Test
-    fun dispatch_onProgressed_thenOnProgressedIsSent() {
+    fun dispatch_onProgressed_sendsEventToCallback() {
         val dispatcher = NavigationEventDispatcher()
         val callback = TestNavigationEventCallback()
         dispatcher.addCallback(callback)
 
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnProgressed(NavigationEvent())
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnProgressed(NavigationEvent())
 
         assertThat(callback.startedInvocations).isEqualTo(0)
         assertThat(callback.progressedInvocations).isEqualTo(1)
@@ -57,14 +69,14 @@
     }
 
     @Test
-    fun dispatch_onCompleted_theOnCompletedIsSent() {
+    fun dispatch_onCompleted_sendsEventToCallback() {
         val dispatcher = NavigationEventDispatcher()
         val callback = TestNavigationEventCallback()
         dispatcher.addCallback(callback)
 
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnCompleted()
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnCompleted()
 
         assertThat(callback.startedInvocations).isEqualTo(0)
         assertThat(callback.progressedInvocations).isEqualTo(0)
@@ -73,14 +85,14 @@
     }
 
     @Test
-    fun dispatch_onCancelled_theOnCancelledIsSent() {
+    fun dispatch_onCancelled_sendsEventToCallback() {
         val dispatcher = NavigationEventDispatcher()
         val callback = TestNavigationEventCallback()
         dispatcher.addCallback(callback)
 
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnCancelled()
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnCancelled()
 
         assertThat(callback.startedInvocations).isEqualTo(0)
         assertThat(callback.progressedInvocations).isEqualTo(0)
@@ -89,26 +101,24 @@
     }
 
     @Test
-    fun removeCallback_whenNavigationIsInProgress_thenOnCancelledIsSent() {
+    fun removeCallback_duringInProgressNavigation_sendsCancellation() {
         val dispatcher = NavigationEventDispatcher()
 
         // We need to capture the state when onEventCancelled is called to verify the order.
         var startedInvocationsAtCancelTime = 0
         val callback =
             TestNavigationEventCallback(
-                onEventCancelled = {
-                    // Capture the count of 'started' invocations when 'cancelled' is called.
-                    startedInvocationsAtCancelTime = this.startedInvocations
-                }
+                onEventCancelled = { startedInvocationsAtCancelTime = this.startedInvocations }
             )
         dispatcher.addCallback(callback)
 
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(NavigationEvent())
-        // Sanity check that navigation has started.
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnStarted(NavigationEvent())
         assertThat(callback.startedInvocations).isEqualTo(1)
 
+        // Removing a callback that is handling an in-progress navigation
+        // must trigger a cancellation event on that callback first.
         callback.remove()
 
         // Assert that onEventCancelled was called once, and it happened after onEventStarted.
@@ -117,15 +127,15 @@
     }
 
     @Test
-    fun dispatch_whenCallbackDisablesItself_thenOnCancelledIsNotSent() {
+    fun dispatch_callbackDisablesItself_doesNotSendCancellation() {
         val dispatcher = NavigationEventDispatcher()
         val callback = TestNavigationEventCallback(onEventStarted = { isEnabled = false })
         dispatcher.addCallback(callback)
 
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(NavigationEvent())
-        inputHandler.handleOnCompleted()
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnStarted(NavigationEvent())
+        input.handleOnCompleted()
 
         // The callback was disabled, but cancellation should not be triggered.
         // The 'completed' event should still be received because the navigation was in progress.
@@ -135,16 +145,18 @@
     }
 
     @Test
-    fun setEnabled_whenNavigationIsInProgress_thenOnCancelledIsNotSent() {
+    fun setEnabled_duringInProgressNavigation_doesNotSendCancellation() {
         val dispatcher = NavigationEventDispatcher()
         val callback = TestNavigationEventCallback()
         dispatcher.addCallback(callback)
 
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(NavigationEvent())
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnStarted(NavigationEvent())
         assertThat(callback.startedInvocations).isEqualTo(1)
 
+        // Disabling a callback should not automatically cancel an in-progress navigation.
+        // This allows UI to be disabled without disrupting an ongoing user action.
         callback.isEnabled = false
 
         // Assert that disabling the callback does not trigger a cancellation.
@@ -152,22 +164,21 @@
     }
 
     @Test
-    fun dispatch_whenCallbackRemovesItselfOnStarted_thenOnCancelledIsSent() {
+    fun dispatch_callbackRemovesItselfOnStarted_sendsCancellation() {
         val dispatcher = NavigationEventDispatcher()
         var cancelledInvocationsAtStartTime = 0
         val callback =
             TestNavigationEventCallback(
                 onEventStarted = {
-                    // Capture the 'cancelled' count before removing to ensure it was 0.
                     cancelledInvocationsAtStartTime = this.cancelledInvocations
                     remove()
                 }
             )
         dispatcher.addCallback(callback)
 
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(NavigationEvent())
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnStarted(NavigationEvent())
 
         // Assert that 'onEventStarted' was called.
         assertThat(callback.startedInvocations).isEqualTo(1)
@@ -178,93 +189,91 @@
     }
 
     @Test
-    fun dispatch_whenAnotherNavigationIsInProgress_thenPreviousIsCancelled() {
+    fun dispatch_newNavigationDuringExisting_cancelsPrevious() {
         val dispatcher = NavigationEventDispatcher()
 
         val callback1 = TestNavigationEventCallback()
         dispatcher.addCallback(callback1)
 
-        // Start the first navigation.
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(NavigationEvent())
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnStarted(NavigationEvent())
         assertThat(callback1.startedInvocations).isEqualTo(1)
 
         val callback2 = TestNavigationEventCallback()
         dispatcher.addCallback(callback2)
 
-        // Start the second navigation, which should cancel the first.
-        inputHandler.handleOnStarted(NavigationEvent())
+        // Starting a new navigation must implicitly cancel any gesture already in progress
+        // to ensure a predictable state.
+        input.handleOnStarted(NavigationEvent())
 
-        // Assert callback1 was cancelled and callback2 was started.
         assertThat(callback1.cancelledInvocations).isEqualTo(1)
         assertThat(callback2.startedInvocations).isEqualTo(1)
 
-        // Complete the second navigation.
-        inputHandler.handleOnCompleted()
+        input.handleOnCompleted()
         assertThat(callback2.completedInvocations).isEqualTo(1)
 
-        // Ensure callback1 was not affected by the completion of the second navigation.
+        // Verify the cancelled callback receives no further events.
         assertThat(callback1.completedInvocations).isEqualTo(0)
     }
 
     @Test
-    fun addCallback_whenNavigationIsInProgress_thenNewCallbackIsIgnoredForCurrentNavigation() {
+    fun addCallback_duringInProgressNavigation_ignoresNewCallbackForCurrentEvent() {
         val dispatcher = NavigationEventDispatcher()
 
         val callback1 = TestNavigationEventCallback()
         dispatcher.addCallback(callback1)
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(NavigationEvent())
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnStarted(NavigationEvent())
         assertThat(callback1.startedInvocations).isEqualTo(1)
 
-        // Add a second callback while the first navigation is in progress.
+        // Add a new callback while a navigation is active.
         val callback2 = TestNavigationEventCallback()
         dispatcher.addCallback(callback2)
 
-        // Complete the first navigation.
-        inputHandler.handleOnCompleted()
+        // The dispatcher should be locked to the callback that started the navigation.
+        // The new callback should not receive the completion event for the current navigation.
+        input.handleOnCompleted()
 
-        // Assert that only the first callback was affected.
         assertThat(callback1.completedInvocations).isEqualTo(1)
         assertThat(callback2.startedInvocations).isEqualTo(0)
         assertThat(callback2.completedInvocations).isEqualTo(0)
 
         // Start and complete a second navigation.
-        inputHandler.handleOnStarted(NavigationEvent())
-        inputHandler.handleOnCompleted()
+        input.handleOnStarted(NavigationEvent())
+        input.handleOnCompleted()
 
-        // Assert that the second navigation was handled by the new top callback (callback2).
-        assertThat(callback1.startedInvocations).isEqualTo(1) // Unchanged
-        assertThat(callback1.completedInvocations).isEqualTo(1) // Unchanged
+        // The second navigation should be handled by the new top callback (callback2).
+        assertThat(callback1.startedInvocations).isEqualTo(1)
+        assertThat(callback1.completedInvocations).isEqualTo(1)
         assertThat(callback2.startedInvocations).isEqualTo(1)
         assertThat(callback2.completedInvocations).isEqualTo(1)
     }
 
     @Test
-    fun dispatch_whenNoEnabledCallbacksExist_thenFallbackIsInvoked() {
+    fun dispatch_withNoEnabledCallbacks_invokesFallback() {
         var fallbackCalled = false
         val dispatcher =
             NavigationEventDispatcher(fallbackOnBackPressed = { fallbackCalled = true })
         val callback = TestNavigationEventCallback()
         dispatcher.addCallback(callback)
 
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnCompleted()
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnCompleted()
         assertThat(callback.completedInvocations).isEqualTo(1)
         assertThat(fallbackCalled).isFalse()
 
-        // After disabling the only callback, the fallback should be called.
+        // After disabling the only callback, the fallback should be triggered.
         callback.isEnabled = false
-        inputHandler.handleOnCompleted()
+        input.handleOnCompleted()
         assertThat(callback.completedInvocations).isEqualTo(1) // Unchanged
         assertThat(fallbackCalled).isTrue()
     }
 
     @Test
-    fun dispatch_whenOverlayCallbackExists_thenOverlaySupersedesDefault() {
+    fun dispatch_withOverlayCallback_prioritizesOverlay() {
         val dispatcher = NavigationEventDispatcher()
         val overlayCallback = TestNavigationEventCallback()
         val normalCallback = TestNavigationEventCallback()
@@ -272,9 +281,9 @@
         dispatcher.addCallback(overlayCallback, NavigationEventPriority.Overlay)
         dispatcher.addCallback(normalCallback, NavigationEventPriority.Default)
 
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnCompleted()
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnCompleted()
 
         // The overlay callback should handle the event, and the normal one should not.
         assertThat(overlayCallback.completedInvocations).isEqualTo(1)
@@ -282,7 +291,7 @@
     }
 
     @Test
-    fun dispatch_whenDisabledOverlayCallbackExists_thenDefaultCallbackIsInvoked() {
+    fun dispatch_withDisabledOverlay_invokesDefaultCallback() {
         val dispatcher = NavigationEventDispatcher()
         val overlayCallback = TestNavigationEventCallback()
         val normalCallback = TestNavigationEventCallback()
@@ -293,9 +302,9 @@
         // The highest priority callback is disabled.
         overlayCallback.isEnabled = false
 
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnCompleted()
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnCompleted()
 
         // The event should skip the disabled overlay and be handled by the default.
         assertThat(overlayCallback.completedInvocations).isEqualTo(0)
@@ -303,11 +312,13 @@
     }
 
     @Test
-    fun addCallback_whenCallbackIsRegisteredWithAnotherDispatcher_thenThrowsException() {
+    fun addCallback_toSecondDispatcher_throwsException() {
         val callback = TestNavigationEventCallback()
         val dispatcher1 = NavigationEventDispatcher()
         dispatcher1.addCallback(callback)
 
+        // A callback cannot be registered to more than one dispatcher at a time
+        // to prevent ambiguous state and ownership issues.
         val dispatcher2 = NavigationEventDispatcher()
         assertThrows<IllegalArgumentException> { dispatcher2.addCallback(callback) }
             .hasMessageThat()
@@ -315,18 +326,19 @@
     }
 
     @Test
-    fun addCallback_whenCallbackIsAlreadyRegistered_thenThrowsException() {
+    fun addCallback_withAlreadyRegisteredCallback_throwsException() {
         val callback = TestNavigationEventCallback()
         val dispatcher = NavigationEventDispatcher()
         dispatcher.addCallback(callback)
 
+        // Adding the same callback instance twice is a developer error and should fail fast.
         assertThrows<IllegalArgumentException> { dispatcher.addCallback(callback) }
             .hasMessageThat()
             .contains("is already registered with a dispatcher")
     }
 
     @Test
-    fun addCallback_whenMultipleOverlayCallbacksExist_thenLastAddedIsInvoked() {
+    fun addCallback_multipleOverlays_prioritizesLastAdded() {
         val dispatcher = NavigationEventDispatcher()
         val firstOverlayCallback = TestNavigationEventCallback()
         val secondOverlayCallback = TestNavigationEventCallback()
@@ -336,9 +348,9 @@
         dispatcher.addCallback(firstOverlayCallback, NavigationEventPriority.Overlay)
         dispatcher.addCallback(secondOverlayCallback, NavigationEventPriority.Overlay)
 
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnCompleted()
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnCompleted()
 
         // Only the last-added overlay callback should handle the event.
         assertThat(secondOverlayCallback.completedInvocations).isEqualTo(1)
@@ -347,53 +359,54 @@
     }
 
     @Test
-    fun dispatch_whenNoCallbacksExist_thenFallbackIsInvoked() {
+    fun dispatch_withNoCallbacks_invokesFallback() {
         var fallbackCalled = false
         val dispatcher =
             NavigationEventDispatcher(fallbackOnBackPressed = { fallbackCalled = true })
 
         // With no callbacks registered at all, the fallback should still work.
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnCompleted()
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnCompleted()
 
         assertThat(fallbackCalled).isTrue()
     }
 
     @Test
-    fun setEnabled_whenDisabledCallbackIsReEnabled_thenItReceivesEvents() {
+    fun setEnabled_onDisabledCallback_reenablesEventReceiving() {
         val dispatcher = NavigationEventDispatcher()
         val callback = TestNavigationEventCallback()
         dispatcher.addCallback(callback)
 
         // Disable the callback and confirm it doesn't receive an event.
         callback.isEnabled = false
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnCompleted()
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnCompleted()
         assertThat(callback.completedInvocations).isEqualTo(0)
 
         // Re-enable the callback.
         callback.isEnabled = true
-        inputHandler.handleOnCompleted()
+        input.handleOnCompleted()
 
         // It should now receive the event.
         assertThat(callback.completedInvocations).isEqualTo(1)
     }
 
     @Test
-    fun dispatch_whenNoNavigationIsInProgress_thenDispatchesToTopCallback() {
+    fun dispatch_withoutStart_sendsToTopCallback() {
         val dispatcher = NavigationEventDispatcher()
         val callback = TestNavigationEventCallback()
         dispatcher.addCallback(callback)
 
         // Dispatching progress or completed without a start should still notify the top callback.
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnProgressed(NavigationEvent())
+        // This handles simple, non-gesture back events.
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnProgressed(NavigationEvent())
         assertThat(callback.progressedInvocations).isEqualTo(1)
 
-        inputHandler.handleOnCompleted()
+        input.handleOnCompleted()
         assertThat(callback.completedInvocations).isEqualTo(1)
 
         // Ensure no cancellation was ever triggered.
@@ -401,167 +414,1108 @@
     }
 
     @Test
-    fun addCallback_whenCallbackIsRemovedAndReAdded_thenBehavesAsNew() {
+    fun addCallback_removedAndReadded_actsAsNew() {
         val dispatcher = NavigationEventDispatcher()
         val callback = TestNavigationEventCallback()
 
         dispatcher.addCallback(callback)
 
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnCompleted()
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnCompleted()
         assertThat(callback.completedInvocations).isEqualTo(1)
 
         // Remove the callback.
         callback.remove()
-        inputHandler.handleOnCompleted()
+        input.handleOnCompleted()
         // Invocations should not increase.
         assertThat(callback.completedInvocations).isEqualTo(1)
 
-        // Re-add the same callback instance. It should be treated as a new callback.
+        // Re-adding the same callback instance should treat it as a new registration.
         dispatcher.addCallback(callback)
-        inputHandler.handleOnCompleted()
+        input.handleOnCompleted()
         // Invocations should increase again.
         assertThat(callback.completedInvocations).isEqualTo(2)
     }
 
     @Test
-    fun addInputHandler_whenAdded_onAttachIsCalled() {
+    fun addInput_onAdd_callsOnAttach() {
         val dispatcher = NavigationEventDispatcher()
-        val handler = TestNavigationEventInput()
+        val input = TestNavigationEventInput()
 
-        dispatcher.addInputHandler(handler)
+        dispatcher.addInput(input)
 
-        assertThat(handler.addedInvocations).isEqualTo(1)
-        assertThat(handler.currentDispatcher).isEqualTo(dispatcher)
+        assertThat(input.addedInvocations).isEqualTo(1)
+        assertThat(input.currentDispatcher).isEqualTo(dispatcher)
     }
 
     @Test
-    fun addInputHandler_whenAddedTwice_onAttachIsCalledOnce() {
+    fun addInput_twice_callsOnAttachOnce() {
         val dispatcher = NavigationEventDispatcher()
-        val handler = TestNavigationEventInput()
+        val input = TestNavigationEventInput()
 
-        dispatcher.addInputHandler(handler)
-        dispatcher.addInputHandler(handler)
+        dispatcher.addInput(input)
+        // Adding the same input again should be idempotent and not re-trigger onAdded.
+        dispatcher.addInput(input)
 
-        assertThat(handler.addedInvocations).isEqualTo(1)
+        assertThat(input.addedInvocations).isEqualTo(1)
     }
 
     @Test
-    fun removeInputHandler_whenRemoved_onDetachIsCalled() {
+    fun removeInput_onRemove_callsOnDetach() {
         val dispatcher = NavigationEventDispatcher()
-        val handler = TestNavigationEventInput()
-        dispatcher.addInputHandler(handler)
-        assertThat(handler.addedInvocations).isEqualTo(1)
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        assertThat(input.addedInvocations).isEqualTo(1)
 
-        dispatcher.removeInputHandler(handler)
+        dispatcher.removeInput(input)
 
-        assertThat(handler.removedInvocations).isEqualTo(1)
-        assertThat(handler.currentDispatcher).isNull()
+        assertThat(input.removedInvocations).isEqualTo(1)
+        assertThat(input.currentDispatcher).isNull()
     }
 
     @Test
-    fun removeInputHandler_whenNotRegistered_onDetachIsNotCalled() {
+    fun removeInput_whenNotRegistered_doesNotCallOnDetach() {
         val dispatcher = NavigationEventDispatcher()
-        val handler = TestNavigationEventInput()
+        val input = TestNavigationEventInput()
 
-        // Try to remove a handler that was never added.
-        dispatcher.removeInputHandler(handler)
+        // Try to remove an input that was never added.
+        dispatcher.removeInput(input)
 
-        assertThat(handler.removedInvocations).isEqualTo(0)
+        assertThat(input.removedInvocations).isEqualTo(0)
     }
 
     @Test
-    fun dispose_detachesInputHandlers() {
+    fun dispose_onCall_detachesInputs() {
         val dispatcher = NavigationEventDispatcher()
-        val handler1 = TestNavigationEventInput()
-        val handler2 = TestNavigationEventInput()
-        dispatcher.addInputHandler(handler1)
-        dispatcher.addInputHandler(handler2)
+        val input1 = TestNavigationEventInput()
+        val input2 = TestNavigationEventInput()
+        dispatcher.addInput(input1)
+        dispatcher.addInput(input2)
 
         dispatcher.dispose()
 
-        assertThat(handler1.removedInvocations).isEqualTo(1)
-        assertThat(handler2.removedInvocations).isEqualTo(1)
-        assertThat(handler1.currentDispatcher).isNull()
-        assertThat(handler2.currentDispatcher).isNull()
+        assertThat(input1.removedInvocations).isEqualTo(1)
+        assertThat(input2.removedInvocations).isEqualTo(1)
+        assertThat(input1.currentDispatcher).isNull()
+        assertThat(input2.currentDispatcher).isNull()
     }
 
     @Test
-    fun addInputHandler_onDisposedDispatcher_throwsException() {
+    fun addInput_onDisposedDispatcher_throws() {
         val dispatcher = NavigationEventDispatcher()
         dispatcher.dispose()
 
-        val handler = TestNavigationEventInput()
-        assertThrows<IllegalStateException> { dispatcher.addInputHandler(handler) }
+        val input = TestNavigationEventInput()
+        assertThrows<IllegalStateException> { dispatcher.addInput(input) }
             .hasMessageThat()
             .contains("This NavigationEventDispatcher has already been disposed")
     }
 
     @Test
-    fun removeInputHandler_onDisposedDispatcher_throwsException() {
+    fun removeInput_onDisposedDispatcher_throws() {
         val dispatcher = NavigationEventDispatcher()
         dispatcher.dispose()
 
-        val handler = TestNavigationEventInput()
-        assertThrows<IllegalStateException> { dispatcher.removeInputHandler(handler) }
+        val input = TestNavigationEventInput()
+        assertThrows<IllegalStateException> { dispatcher.removeInput(input) }
             .hasMessageThat()
             .contains("This NavigationEventDispatcher has already been disposed")
     }
 
     @Test
-    fun onHasEnabledCallbacksChanged_whenHandlerIsRegistered_callbackIsFired() {
+    fun onHasEnabledCallbacksChanged_onCallbackChange_notifiesInput() {
         val dispatcher = NavigationEventDispatcher()
-        val handler = TestNavigationEventInput()
-        dispatcher.addInputHandler(handler)
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
 
-        assertThat(handler.onHasEnabledCallbacksChangedInvocations).isEqualTo(0)
+        assertThat(input.onHasEnabledCallbacksChangedInvocations).isEqualTo(0)
 
-        // Add a callback to trigger the onHasEnabledCallbacksChanged listener
+        // Adding a callback should trigger the onHasEnabledCallbacksChanged listener.
         val callback = TestNavigationEventCallback()
         dispatcher.addCallback(callback)
 
-        assertThat(handler.onHasEnabledCallbacksChangedInvocations).isEqualTo(1)
+        assertThat(input.onHasEnabledCallbacksChangedInvocations).isEqualTo(1)
 
-        // Disable the callback to trigger it again
+        // Disabling the callback should trigger it again.
         callback.isEnabled = false
-        assertThat(handler.onHasEnabledCallbacksChangedInvocations).isEqualTo(2)
+        assertThat(input.onHasEnabledCallbacksChangedInvocations).isEqualTo(2)
     }
 
     @Test
-    fun onHasEnabledCallbacksChanged_whenHandlerIsRemoved_callbackIsNotFired() {
+    fun onHasEnabledCallbacksChanged_afterInputRemoved_doesNotNotify() {
         val dispatcher = NavigationEventDispatcher()
-        val handler = TestNavigationEventInput()
-        dispatcher.addInputHandler(handler)
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
 
         val callback1 = TestNavigationEventCallback()
         dispatcher.addCallback(callback1)
-        assertThat(handler.onHasEnabledCallbacksChangedInvocations).isEqualTo(1)
+        assertThat(input.onHasEnabledCallbacksChangedInvocations).isEqualTo(1)
 
-        dispatcher.removeInputHandler(handler)
+        dispatcher.removeInput(input)
 
-        // Add another callback; the removed handler should not be notified
+        // Add another callback; the removed input should not be notified.
         val callback2 = TestNavigationEventCallback()
         dispatcher.addCallback(callback2)
-        assertThat(handler.onHasEnabledCallbacksChangedInvocations).isEqualTo(1) // Unchanged
+        assertThat(input.onHasEnabledCallbacksChangedInvocations).isEqualTo(1) // Unchanged
     }
 
     @Test
-    fun dispose_whenParentIsDisposed_detachesChildInputHandlers() {
+    fun dispose_onParent_detachesChildInputs() {
         val parentDispatcher = NavigationEventDispatcher()
         val childDispatcher = NavigationEventDispatcher(parentDispatcher)
 
-        val parentHandler = TestNavigationEventInput()
-        val childHandler = TestNavigationEventInput()
-        parentDispatcher.addInputHandler(parentHandler)
-        childDispatcher.addInputHandler(childHandler)
+        val parentInput = TestNavigationEventInput()
+        val childInput = TestNavigationEventInput()
+        parentDispatcher.addInput(parentInput)
+        childDispatcher.addInput(childInput)
 
-        // Disposing the parent should cascade to the child
+        // Disposing the parent should cascade to the child.
         parentDispatcher.dispose()
 
-        assertThat(parentHandler.removedInvocations).isEqualTo(1)
-        assertThat(childHandler.removedInvocations).isEqualTo(1)
-        assertThat(parentHandler.currentDispatcher).isNull()
-        assertThat(childHandler.currentDispatcher).isNull()
+        assertThat(parentInput.removedInvocations).isEqualTo(1)
+        assertThat(childInput.removedInvocations).isEqualTo(1)
+        assertThat(parentInput.currentDispatcher).isNull()
+        assertThat(childInput.currentDispatcher).isNull()
+    }
+
+    // endregion Core API
+
+    // region Passive Listeners API
+
+    @Test
+    fun state_onMultipleCallbacksAdded_reflectsLastAdded() = runTest {
+        val dispatcher = NavigationEventDispatcher()
+        val homeCallback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("home"))
+        val detailsCallback =
+            TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
+
+        assertThat(dispatcher.state.value).isEqualTo(Idle(NotProvided))
+
+        dispatcher.addCallback(homeCallback)
+        assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("home")))
+
+        // Callbacks are prioritized like a stack (LIFO), so adding a new one makes it active.
+        dispatcher.addCallback(detailsCallback)
+        assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("details")))
+    }
+
+    @Test
+    fun state_onSetInfoOnActiveCallback_updatesState() = runTest {
+        val dispatcher = NavigationEventDispatcher()
+        val callback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("initial"))
+        dispatcher.addCallback(callback)
+
+        assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("initial")))
+
+        // Calling setInfo on the active callback should immediately update the dispatcher's state.
+        callback.setInfo(currentInfo = HomeScreenInfo("updated"), previousInfo = null)
+
+        assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("updated")))
+    }
+
+    @Test
+    fun state_onSetInfoOnInactiveCallback_doesNotUpdateState() = runTest {
+        val dispatcher = NavigationEventDispatcher()
+        val homeCallback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("home"))
+        val detailsCallback =
+            TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
+        dispatcher.addCallback(homeCallback)
+        dispatcher.addCallback(detailsCallback)
+
+        // The state should reflect the last-added (active) callback.
+        assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("details")))
+
+        // Calling setInfo on an inactive callback should NOT affect the global state.
+        homeCallback.setInfo(currentInfo = HomeScreenInfo("home-updated"), previousInfo = null)
+
+        // The state should remain unchanged because the update came from a non-active callback.
+        assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("details")))
+    }
+
+    @Test
+    fun state_onFullGestureLifecycle_transitionsToInProgressThenIdle() {
+        val dispatcher = NavigationEventDispatcher()
+        val input = TestNavigationEventInput().also { dispatcher.addInput(it) }
+        val callbackInfo = HomeScreenInfo("home")
+        val callback = TestNavigationEventCallback(currentInfo = callbackInfo)
+        dispatcher.addCallback(callback)
+
+        val startEvent = NavigationEvent(touchX = 0.1F)
+        val progressEvent = NavigationEvent(touchX = 0.3f)
+
+        assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
+
+        // Starting a gesture should move the state to InProgress with the start event.
+        input.handleOnStarted(startEvent)
+        var state = dispatcher.state.value as InProgress
+        assertThat(state.currentInfo).isEqualTo(callbackInfo)
+        assertThat(state.previousInfo).isNull()
+        assertThat(state.latestEvent).isEqualTo(startEvent)
+
+        // Progressing the gesture should keep it InProgress but update to the latest event.
+        input.handleOnProgressed(progressEvent)
+        state = dispatcher.state.value as InProgress
+        assertThat(state.latestEvent).isEqualTo(progressEvent)
+
+        // Completing the gesture should return the state to Idle.
+        input.handleOnCompleted()
+        assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
+    }
+
+    @Test
+    fun state_onGestureCancelled_returnsToIdle() {
+        val dispatcher = NavigationEventDispatcher()
+        val input = TestNavigationEventInput().also { dispatcher.addInput(it) }
+        val callbackInfo = HomeScreenInfo("home")
+        val callback = TestNavigationEventCallback(currentInfo = callbackInfo)
+        dispatcher.addCallback(callback)
+
+        val startEvent = NavigationEvent()
+
+        assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
+
+        // Starting a gesture moves the state to InProgress.
+        input.handleOnStarted(startEvent)
+        assertThat(dispatcher.state.value).isEqualTo(InProgress(callbackInfo, null, startEvent))
+
+        // Cancelling the gesture should also return the state to Idle.
+        input.handleOnCancelled()
+        assertThat(dispatcher.state.value).isEqualTo(Idle(callbackInfo))
+    }
+
+    @Test
+    fun inProgressState_onInfoUpdateDuringGesture_reflectsNewInfo() {
+        val dispatcher = NavigationEventDispatcher()
+        val input = TestNavigationEventInput().also { dispatcher.addInput(it) }
+        val firstInfo = HomeScreenInfo("initial")
+        val callback = TestNavigationEventCallback(currentInfo = firstInfo)
+        dispatcher.addCallback(callback)
+
+        val startEvent = NavigationEvent(touchX = 0.1F)
+
+        input.handleOnStarted(startEvent)
+
+        // At the start, previousInfo is null.
+        var state = dispatcher.state.value as InProgress
+        assertThat(state.currentInfo).isEqualTo(firstInfo)
+        assertThat(state.previousInfo).isNull()
+        assertThat(state.latestEvent).isEqualTo(startEvent)
+
+        // Update the info mid-gesture.
+        val secondInfo = HomeScreenInfo("updated")
+        callback.setInfo(currentInfo = secondInfo, previousInfo = firstInfo)
+
+        // The state should now reflect the updated info. The `previousInfo` is now captured.
+        state = dispatcher.state.value as InProgress
+        assertThat(state.currentInfo).isEqualTo(secondInfo)
+        assertThat(state.previousInfo).isEqualTo(firstInfo)
+        assertThat(state.latestEvent).isEqualTo(startEvent) // Event hasn't changed yet.
+
+        // Complete the gesture.
+        input.handleOnCompleted()
+        assertThat(dispatcher.state.value).isEqualTo(Idle(secondInfo))
+    }
+
+    @Test
+    fun inProgressState_onNewGesture_clearsPreviousInfo() {
+        val dispatcher = NavigationEventDispatcher()
+        val input = TestNavigationEventInput().also { dispatcher.addInput(it) }
+        val initialInfo = HomeScreenInfo("initial")
+        val callback = TestNavigationEventCallback(currentInfo = initialInfo)
+        dispatcher.addCallback(callback)
+
+        // FIRST GESTURE: Create a complex state.
+        input.handleOnStarted(NavigationEvent(touchX = 0.1f))
+        callback.setInfo(currentInfo = HomeScreenInfo("updated"), previousInfo = null)
+        input.handleOnCompleted()
+
+        // After the first gesture, the final state is Idle with the updated info.
+        val finalInfo = HomeScreenInfo("updated")
+        assertThat(dispatcher.state.value).isEqualTo(Idle(finalInfo))
+
+        // SECOND GESTURE: Start a new gesture.
+        val event2 = NavigationEvent(touchX = 0.3f)
+        input.handleOnStarted(event2)
+
+        // When a new gesture starts, `previousInfo` must be null, not stale data
+        // from a previous, completed gesture.
+        val state = dispatcher.state.value as InProgress
+        assertThat(state.currentInfo).isEqualTo(finalInfo)
+        assertThat(state.previousInfo).isNull()
+        assertThat(state.latestEvent).isEqualTo(event2)
+    }
+
+    @Test
+    fun state_onDispatcherDisabled_fallsBackToSibling() {
+        val dispatcher = NavigationEventDispatcher()
+        val childDispatcher = NavigationEventDispatcher(parentDispatcher = dispatcher)
+
+        val callbackA = TestNavigationEventCallback(currentInfo = HomeScreenInfo("A"))
+        dispatcher.addCallback(callbackA)
+        assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("A")))
+
+        val callbackB = TestNavigationEventCallback(currentInfo = DetailsScreenInfo("B"))
+        childDispatcher.addCallback(callbackB)
+        // Assert that state reflects callbackB, which was added last and is now active.
+        assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("B")))
+
+        // Disable the dispatcher that hosts the currently active callback.
+        childDispatcher.isEnabled = false
+
+        // The state should now fall back to the next-highest priority callback (callbackA).
+        assertThat(dispatcher.state.value).isEqualTo(Idle(HomeScreenInfo("A")))
+
+        // Re-enable the dispatcher.
+        childDispatcher.isEnabled = true
+
+        // The state should once again reflect callbackB.
+        assertThat(dispatcher.state.value).isEqualTo(Idle(DetailsScreenInfo("B")))
+    }
+
+    @Test
+    fun getState_withTypeFilter_emitsOnlyMatchingStates() =
+        runTest(UnconfinedTestDispatcher()) {
+            val dispatcher = NavigationEventDispatcher()
+            val initialHomeInfo = HomeScreenInfo("initial")
+            val homeCallback = TestNavigationEventCallback(currentInfo = HomeScreenInfo("home"))
+            val detailsCallback =
+                TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
+            val collectedStates = mutableListOf<NavigationEventState<HomeScreenInfo>>()
+
+            dispatcher
+                .getState(backgroundScope, initialHomeInfo)
+                .onEach { collectedStates.add(it) }
+                .launchIn(backgroundScope)
+            advanceUntilIdle()
+
+            // The flow must start with the initial value provided.
+            assertThat(collectedStates).hasSize(1)
+            assertThat(collectedStates.last()).isEqualTo(Idle(initialHomeInfo))
+
+            // A new state with a matching type should be collected.
+            dispatcher.addCallback(homeCallback)
+            advanceUntilIdle()
+            assertThat(collectedStates).hasSize(2)
+            assertThat(collectedStates.last()).isEqualTo(Idle(HomeScreenInfo("home")))
+
+            // A state with a non-matching type should be filtered out and not collected.
+            dispatcher.addCallback(detailsCallback)
+            advanceUntilIdle()
+            assertThat(collectedStates).hasSize(2)
+
+            // When the active callback is removed, the state falls back to a matching type,
+            // but since the info is the same as before, no new state is emitted.
+            detailsCallback.remove()
+            advanceUntilIdle()
+            assertThat(collectedStates).hasSize(2)
+            assertThat(collectedStates.last()).isEqualTo(Idle(HomeScreenInfo("home")))
+        }
+
+    @Test
+    fun getState_withNonMatchingType_emitsOnlyInitialInfo() =
+        runTest(UnconfinedTestDispatcher()) {
+            val dispatcher = NavigationEventDispatcher()
+            val initialHomeInfo = HomeScreenInfo("initial")
+            val detailsCallback =
+                TestNavigationEventCallback(currentInfo = DetailsScreenInfo("details"))
+            val collectedStates = mutableListOf<NavigationEventState<HomeScreenInfo>>()
+
+            dispatcher
+                .getState(backgroundScope, initialHomeInfo)
+                .onEach { collectedStates.add(it) }
+                .launchIn(backgroundScope)
+            advanceUntilIdle()
+
+            // The flow must start with its initial value.
+            assertThat(collectedStates).hasSize(1)
+            assertThat(collectedStates.first()).isEqualTo(Idle(initialHomeInfo))
+
+            // Add a callback with a non-matching type.
+            dispatcher.addCallback(detailsCallback)
+            advanceUntilIdle()
+
+            // The collector should not have emitted a new value.
+            assertThat(collectedStates).hasSize(1)
+
+            // Update the non-matching callback's info.
+            detailsCallback.setInfo(
+                currentInfo = DetailsScreenInfo("details-updated"),
+                previousInfo = null,
+            )
+            advanceUntilIdle()
+
+            // The collector should still not have emitted a new value.
+            assertThat(collectedStates).hasSize(1)
+        }
+
+    @Test
+    fun progress_inIdleAndInProgress_returnsCorrectValue() {
+        val dispatcher = NavigationEventDispatcher()
+        val input = TestNavigationEventInput().also { dispatcher.addInput(it) }
+        val callbackInfo = HomeScreenInfo("home")
+        val callback = TestNavigationEventCallback(currentInfo = callbackInfo)
+        dispatcher.addCallback(callback)
+
+        // Before any gesture, the state is Idle and progress should be 0.
+        assertThat(dispatcher.state.value.progress).isEqualTo(0f)
+
+        // Start a gesture.
+        input.handleOnStarted(NavigationEvent(progress = 0.1f))
+        assertThat(dispatcher.state.value.progress).isEqualTo(0.1f)
+
+        // InProgress state should reflect the event's progress.
+        input.handleOnProgressed(NavigationEvent(progress = 0.5f))
+        assertThat(dispatcher.state.value.progress).isEqualTo(0.5f)
+
+        // Complete the gesture.
+        input.handleOnCompleted()
+
+        // After the gesture, the state is Idle again and progress should be 0.
+        assertThat(dispatcher.state.value.progress).isEqualTo(0f)
+    }
+
+    // endregion
+
+    // region Hierarchy APIs
+
+    @Test
+    fun init_withParent_sharesCallbacks() {
+        val parentDispatcher = NavigationEventDispatcher()
+        val parentCallback = TestNavigationEventCallback()
+        parentDispatcher.addCallback(parentCallback)
+
+        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+        val childCallback = TestNavigationEventCallback()
+        childDispatcher.addCallback(childCallback)
+
+        val event = NavigationEvent()
+        val input = TestNavigationEventInput()
+        parentDispatcher.addInput(input)
+        input.handleOnStarted(event)
+
+        // Callbacks from child dispatchers are prioritized over their parents (LIFO).
+        assertThat(parentCallback.startedInvocations).isEqualTo(0)
+        assertThat(childCallback.startedInvocations).isEqualTo(1)
+    }
+
+    @Test
+    fun init_withoutParent_hasIndependentCallbacks() {
+        val parentDispatcher = NavigationEventDispatcher()
+        val childDispatcher = NavigationEventDispatcher()
+
+        val parentCallback = TestNavigationEventCallback()
+        val childCallback = TestNavigationEventCallback()
+        parentDispatcher.addCallback(parentCallback)
+        childDispatcher.addCallback(childCallback)
+
+        // Dispatch an event through the parent.
+        val event = NavigationEvent()
+        val parentInput = TestNavigationEventInput()
+        parentDispatcher.addInput(parentInput)
+        parentInput.handleOnStarted(event)
+
+        // Only the parent's callback should be invoked.
+        assertThat(parentCallback.startedInvocations).isEqualTo(1)
+        assertThat(childCallback.startedInvocations).isEqualTo(0)
+
+        // Dispatch an event through the child.
+        val childInput = TestNavigationEventInput()
+        childDispatcher.addInput(childInput)
+        childInput.handleOnStarted(event)
+
+        // Only the child's callback should be invoked.
+        assertThat(parentCallback.startedInvocations).isEqualTo(1)
+        assertThat(childCallback.startedInvocations).isEqualTo(1)
+    }
+
+    @Test
+    fun addCallback_toChild_isDispatchedViaParent() {
+        val parentDispatcher = NavigationEventDispatcher()
+        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+        val callback = TestNavigationEventCallback()
+
+        childDispatcher.addCallback(callback)
+
+        // Events dispatched from a parent should propagate to callbacks in child dispatchers.
+        val event = NavigationEvent()
+        val input = TestNavigationEventInput()
+        parentDispatcher.addInput(input)
+        input.handleOnStarted(event)
+        assertThat(callback.startedInvocations).isEqualTo(1)
+    }
+
+    @Test
+    fun addCallback_toParentThenChild_ordersLIFO() {
+        val parentDispatcher = NavigationEventDispatcher()
+        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+        val parentCallback = TestNavigationEventCallback()
+        val childCallback = TestNavigationEventCallback()
+
+        parentDispatcher.addCallback(parentCallback)
+        childDispatcher.addCallback(childCallback)
+
+        // The last-added callback (child's) should be invoked first.
+        val event = NavigationEvent()
+        val input = TestNavigationEventInput()
+        parentDispatcher.addInput(input)
+        input.handleOnStarted(event)
+
+        assertThat(parentCallback.startedInvocations).isEqualTo(0)
+        assertThat(childCallback.startedInvocations).isEqualTo(1)
+    }
+
+    @Test
+    fun addCallback_multipleDispatchers_prioritizesLastAdded() {
+        val parentDispatcher = NavigationEventDispatcher()
+        val child1Dispatcher = NavigationEventDispatcher(parentDispatcher)
+        val child2Dispatcher = NavigationEventDispatcher(parentDispatcher)
+
+        val parentCallback = TestNavigationEventCallback()
+        val childCallback1 = TestNavigationEventCallback()
+        val childCallback2 = TestNavigationEventCallback()
+
+        parentDispatcher.addCallback(parentCallback)
+        child2Dispatcher.addCallback(childCallback2)
+        child1Dispatcher.addCallback(childCallback1)
+
+        // Callbacks are processed in a LIFO manner across the entire hierarchy.
+        // The callback from child1 was added last, so it gets the event.
+        val event = NavigationEvent()
+        val input = TestNavigationEventInput()
+        parentDispatcher.addInput(input)
+        input.handleOnStarted(event)
+
+        assertThat(parentCallback.startedInvocations).isEqualTo(0)
+        assertThat(childCallback2.startedInvocations).isEqualTo(0)
+        assertThat(childCallback1.startedInvocations).isEqualTo(1)
+    }
+
+    @Test
+    fun dispose_onChild_parentStillReceivesEvents() {
+        val parentDispatcher = NavigationEventDispatcher()
+        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+        val parentCallback = TestNavigationEventCallback()
+        val childCallback = TestNavigationEventCallback()
+        parentDispatcher.addCallback(parentCallback)
+        childDispatcher.addCallback(childCallback)
+
+        // Disposing a child should not affect its parent.
+        childDispatcher.dispose()
+
+        // Dispatching an event from the parent should now trigger the parent's callback,
+        // as the child's (previously higher priority) callback is gone.
+        val event = NavigationEvent()
+        val input = TestNavigationEventInput()
+        parentDispatcher.addInput(input)
+        input.handleOnStarted(event)
+        assertThat(parentCallback.startedInvocations).isEqualTo(1)
+        assertThat(childCallback.startedInvocations).isEqualTo(0)
+    }
+
+    @Test
+    fun dispose_onParent_cascadesAndDisablesChildren() {
+        val parentDispatcher = NavigationEventDispatcher()
+        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+
+        parentDispatcher.dispose()
+
+        // Attempting to use either dispatcher should now fail.
+        val event = NavigationEvent()
+        assertThrows<IllegalStateException> {
+                val input = TestNavigationEventInput()
+                parentDispatcher.addInput(input)
+                input.handleOnStarted(event)
+            }
+            .hasMessageThat()
+            .contains("has already been disposed")
+        assertThrows<IllegalStateException> {
+                val input = TestNavigationEventInput()
+                childDispatcher.addInput(input)
+                input.handleOnStarted(event)
+            }
+            .hasMessageThat()
+            .contains("has already been disposed")
+    }
+
+    @Test
+    fun dispose_onGrandparent_cascadesAndDisablesHierarchy() {
+        val grandparentDispatcher = NavigationEventDispatcher()
+        val parentDispatcher = NavigationEventDispatcher(grandparentDispatcher)
+        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+
+        // Disposing the root dispatcher should disable the entire hierarchy.
+        grandparentDispatcher.dispose()
+
+        // Attempting to use any dispatcher in the hierarchy should fail.
+        val event = NavigationEvent()
+        assertThrows<IllegalStateException> {
+                val input = TestNavigationEventInput()
+                grandparentDispatcher.addInput(input)
+                input.handleOnStarted(event)
+            }
+            .hasMessageThat()
+            .contains("has already been disposed")
+        assertThrows<IllegalStateException> {
+                val input = TestNavigationEventInput()
+                parentDispatcher.addInput(input)
+                input.handleOnStarted(event)
+            }
+            .hasMessageThat()
+            .contains("has already been disposed")
+        assertThrows<IllegalStateException> {
+                val input = TestNavigationEventInput()
+                childDispatcher.addInput(input)
+                input.handleOnStarted(event)
+            }
+            .hasMessageThat()
+            .contains("has already been disposed")
+    }
+
+    @Test
+    fun isEnabled_whenTrue_dispatchesEvents() {
+        val dispatcher = NavigationEventDispatcher()
+        val callback = TestNavigationEventCallback()
+        dispatcher.addCallback(callback)
+
+        dispatcher.isEnabled = true
+
+        val event = NavigationEvent()
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnStarted(event)
+        assertThat(callback.startedInvocations).isEqualTo(1)
+    }
+
+    @Test
+    fun isEnabled_whenFalse_doesNotDispatchEvents() {
+        val dispatcher = NavigationEventDispatcher()
+        val callback = TestNavigationEventCallback(isEnabled = true)
+        dispatcher.addCallback(callback)
+
+        dispatcher.isEnabled = false
+
+        val event = NavigationEvent()
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnStarted(event)
+        assertThat(callback.startedInvocations).isEqualTo(0)
+    }
+
+    @Test
+    fun isEnabled_parentDisabled_disablesChildDispatch() {
+        val parentDispatcher = NavigationEventDispatcher()
+        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+        val parentCallback = TestNavigationEventCallback()
+        val childCallback = TestNavigationEventCallback()
+        parentDispatcher.addCallback(parentCallback)
+        childDispatcher.addCallback(childCallback)
+
+        // Disabling the parent should effectively disable the entire sub-hierarchy.
+        parentDispatcher.isEnabled = false
+
+        val event = NavigationEvent()
+        val input = TestNavigationEventInput()
+        childDispatcher.addInput(input)
+        input.handleOnStarted(event)
+
+        assertThat(parentCallback.startedInvocations).isEqualTo(0)
+        assertThat(childCallback.startedInvocations).isEqualTo(0)
+    }
+
+    @Test
+    fun isEnabled_childDisabled_doesNotDispatch() {
+        val parentDispatcher = NavigationEventDispatcher()
+        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+        val parentCallback = TestNavigationEventCallback()
+        val childCallback = TestNavigationEventCallback()
+        parentDispatcher.addCallback(parentCallback)
+        childDispatcher.addCallback(childCallback)
+
+        childDispatcher.isEnabled = false
+
+        // Events sent to the child dispatcher should not be processed by any callback.
+        val event = NavigationEvent()
+        val input = TestNavigationEventInput()
+        childDispatcher.addInput(input)
+        input.handleOnStarted(event)
+
+        assertThat(childCallback.startedInvocations).isEqualTo(0)
+        assertThat(parentCallback.startedInvocations).isEqualTo(0)
+    }
+
+    @Test
+    fun isEnabled_childDisabled_parentStillDispatches() {
+        val parentDispatcher = NavigationEventDispatcher()
+        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+        val parentCallback = TestNavigationEventCallback()
+        val childCallback = TestNavigationEventCallback()
+        parentDispatcher.addCallback(parentCallback)
+        childDispatcher.addCallback(childCallback)
+
+        childDispatcher.isEnabled = false
+
+        // Disabling a child should not affect the parent. Events sent directly to the
+        // parent should be handled by the parent's callbacks.
+        val event = NavigationEvent()
+        val input = TestNavigationEventInput()
+        parentDispatcher.addInput(input)
+        input.handleOnStarted(event)
+
+        assertThat(childCallback.startedInvocations).isEqualTo(0)
+        assertThat(parentCallback.startedInvocations).isEqualTo(1)
+    }
+
+    @Test
+    fun isEnabled_parentReenabled_reenablesChildDispatch() {
+        val parentDispatcher = NavigationEventDispatcher()
+        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+        val parentCallback = TestNavigationEventCallback()
+        val childCallback = TestNavigationEventCallback()
+        parentDispatcher.addCallback(parentCallback)
+        childDispatcher.addCallback(childCallback)
+
+        parentDispatcher.isEnabled = false
+        val initialEvent = NavigationEvent()
+        val input = TestNavigationEventInput()
+        childDispatcher.addInput(input)
+        input.handleOnStarted(initialEvent)
+        assertThat(childCallback.startedInvocations).isEqualTo(0)
+
+        parentDispatcher.isEnabled = true
+
+        val reEnabledEvent = NavigationEvent()
+        input.handleOnStarted(reEnabledEvent)
+
+        assertThat(childCallback.startedInvocations).isEqualTo(1)
+        assertThat(parentCallback.startedInvocations).isEqualTo(0)
+    }
+
+    @Test
+    fun isEnabled_parentReenabled_childCallbackReceivesEvents() {
+        val parentDispatcher = NavigationEventDispatcher()
+        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+        val parentCallback = TestNavigationEventCallback()
+        val childCallback = TestNavigationEventCallback()
+        parentDispatcher.addCallback(parentCallback)
+        childDispatcher.addCallback(childCallback)
+
+        parentDispatcher.isEnabled = false
+        val initialEvent = NavigationEvent()
+        val input = TestNavigationEventInput()
+        parentDispatcher.addInput(input)
+        input.handleOnStarted(initialEvent)
+        assertThat(parentCallback.startedInvocations).isEqualTo(0)
+        assertThat(childCallback.startedInvocations).isEqualTo(0)
+
+        parentDispatcher.isEnabled = true
+
+        val reEnabledEvent = NavigationEvent()
+        input.handleOnStarted(reEnabledEvent)
+        assertThat(parentCallback.startedInvocations).isEqualTo(0)
+        assertThat(childCallback.startedInvocations).isEqualTo(1)
+    }
+
+    @Test
+    fun isEnabled_grandparentDisabled_disablesGrandchildDispatch() {
+        val grandparentDispatcher = NavigationEventDispatcher()
+        val parentDispatcher = NavigationEventDispatcher(grandparentDispatcher)
+        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+        val grandparentCallback = TestNavigationEventCallback()
+        val parentCallback = TestNavigationEventCallback()
+        val childCallback = TestNavigationEventCallback()
+
+        grandparentDispatcher.addCallback(grandparentCallback)
+        parentDispatcher.addCallback(parentCallback)
+        childDispatcher.addCallback(childCallback)
+
+        grandparentDispatcher.isEnabled = false
+
+        // Disabling the grandparent should disable all descendants.
+        val event = NavigationEvent()
+        val input = TestNavigationEventInput()
+        childDispatcher.addInput(input)
+        input.handleOnStarted(event)
+
+        assertThat(grandparentCallback.startedInvocations).isEqualTo(0)
+        assertThat(parentCallback.startedInvocations).isEqualTo(0)
+        assertThat(childCallback.startedInvocations).isEqualTo(0)
+    }
+
+    @Test
+    fun isEnabled_grandparentDisabled_grandchildCallbackDoesNotReceiveEvents() {
+        val grandparentDispatcher = NavigationEventDispatcher()
+        val parentDispatcher = NavigationEventDispatcher(grandparentDispatcher)
+        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
+        val grandparentCallback = TestNavigationEventCallback()
+        val parentCallback = TestNavigationEventCallback()
+        val childCallback = TestNavigationEventCallback()
+
+        grandparentDispatcher.addCallback(grandparentCallback)
+        parentDispatcher.addCallback(parentCallback)
+        childDispatcher.addCallback(childCallback)
+
+        grandparentDispatcher.isEnabled = false
+
+        val event = NavigationEvent()
+        val input = TestNavigationEventInput()
+        grandparentDispatcher.addInput(input)
+        input.handleOnStarted(event)
+
+        assertThat(grandparentCallback.startedInvocations).isEqualTo(0)
+        assertThat(parentCallback.startedInvocations).isEqualTo(0)
+        assertThat(childCallback.startedInvocations).isEqualTo(0)
+    }
+
+    @Test
+    fun callbackIsEnabled_whenDispatcherDisabled_doesNotReceiveEvents() {
+        val dispatcher = NavigationEventDispatcher()
+        val callback = TestNavigationEventCallback()
+        dispatcher.addCallback(callback)
+        val preDisableEvent = NavigationEvent()
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnStarted(preDisableEvent)
+        assertThat(callback.startedInvocations).isEqualTo(1)
+
+        dispatcher.isEnabled = false
+
+        // An enabled callback on a disabled dispatcher should not receive events.
+        val event = NavigationEvent()
+        input.handleOnStarted(event)
+
+        assertThat(callback.startedInvocations).isEqualTo(1)
+    }
+
+    @Test
+    fun callbackIsEnabled_whenDispatcherReenabled_receivesEvents() {
+        val dispatcher = NavigationEventDispatcher()
+        val callback = TestNavigationEventCallback()
+        dispatcher.addCallback(callback)
+        dispatcher.isEnabled = false
+
+        val preEnableEvent = NavigationEvent()
+        val input = TestNavigationEventInput()
+        dispatcher.addInput(input)
+        input.handleOnStarted(preEnableEvent)
+        assertThat(callback.startedInvocations).isEqualTo(0)
+
+        dispatcher.isEnabled = true
+
+        val reEnabledEvent = NavigationEvent()
+        input.handleOnStarted(reEnabledEvent)
+
+        assertThat(callback.startedInvocations).isEqualTo(1)
+    }
+
+    @Test
+    fun addCallback_onDisposedDispatcher_throws() {
+        val dispatcher = NavigationEventDispatcher()
+        dispatcher.dispose()
+
+        assertThrows<IllegalStateException> {
+                dispatcher.addCallback(TestNavigationEventCallback())
+            }
+            .hasMessageThat()
+            .contains("has already been disposed")
+    }
+
+    @Test
+    fun handleOnStarted_onDisposedDispatcher_throws() {
+        val dispatcher = NavigationEventDispatcher()
+        dispatcher.dispose()
+
+        assertThrows<IllegalStateException> {
+                val input = TestNavigationEventInput()
+                dispatcher.addInput(input)
+                input.handleOnStarted(NavigationEvent())
+            }
+            .hasMessageThat()
+            .contains("has already been disposed")
+    }
+
+    @Test
+    fun handleOnProgressed_onDisposedDispatcher_throws() {
+        val dispatcher = NavigationEventDispatcher()
+        dispatcher.dispose()
+
+        assertThrows<IllegalStateException> {
+                val input = TestNavigationEventInput()
+                dispatcher.addInput(input)
+                input.handleOnProgressed(NavigationEvent())
+            }
+            .hasMessageThat()
+            .contains("has already been disposed")
+    }
+
+    @Test
+    fun handleOnCompleted_onDisposedDispatcher_throws() {
+        val dispatcher = NavigationEventDispatcher()
+        dispatcher.dispose()
+
+        assertThrows<IllegalStateException> {
+                val input = TestNavigationEventInput()
+                dispatcher.addInput(input)
+                input.handleOnCompleted()
+            }
+            .hasMessageThat()
+            .contains("has already been disposed")
+    }
+
+    @Test
+    fun handleOnCancelled_onDisposedDispatcher_throws() {
+        val dispatcher = NavigationEventDispatcher()
+        dispatcher.dispose()
+
+        assertThrows<IllegalStateException> {
+                val input = TestNavigationEventInput()
+                dispatcher.addInput(input)
+                input.handleOnCancelled()
+            }
+            .hasMessageThat()
+            .contains("has already been disposed")
+    }
+
+    @Test
+    fun dispose_onDisposedDispatcher_throws() {
+        val dispatcher = NavigationEventDispatcher()
+        dispatcher.dispose()
+
+        // Disposing an already-disposed dispatcher should fail.
+        assertThrows<IllegalStateException> { dispatcher.dispose() }
+            .hasMessageThat()
+            .contains("has already been disposed")
+    }
+
+    @Test
+    fun setEnabled_onDisposedDispatcher_throws() {
+        val dispatcher = NavigationEventDispatcher()
+        dispatcher.dispose()
+
+        assertThrows<IllegalStateException> { dispatcher.isEnabled = false }
+            .hasMessageThat()
+            .contains("has already been disposed")
+    }
+
+    @Test
+    fun setDisabled_onDisposedDispatcher_throws() {
+        val dispatcher = NavigationEventDispatcher()
+        dispatcher.dispose()
+
+        assertThrows<IllegalStateException> { dispatcher.isEnabled = false }
+            .hasMessageThat()
+            .contains("has already been disposed")
+    }
+
+    // endregion Hierarchy APIs
+}
+
+/** A sealed interface for type-safe navigation information. */
+sealed interface TestInfo : NavigationEventInfo
+
+data class HomeScreenInfo(val id: String) : TestInfo
+
+data class DetailsScreenInfo(val id: String) : TestInfo
+
+/**
+ * A test implementation of [NavigationEventInput] that records lifecycle events and invocation
+ * counts.
+ *
+ * Use this class in tests to verify that `onAdded`, `onRemoved`, and `onHasEnabledCallbacksChanged`
+ * are called correctly. It counts how many times each lifecycle method is invoked and stores a
+ * reference to the most recently added dispatcher. It also provides helper methods to simulate
+ * dispatching navigation events.
+ *
+ * @param onAdded An optional lambda to execute when [onAdded] is called.
+ * @param onRemoved An optional lambda to execute when [onRemoved] is called.
+ * @param onHasEnabledCallbacksChanged An optional lambda to execute when
+ *   [onHasEnabledCallbacksChanged] is called.
+ */
+private class TestNavigationEventInput(
+    private val onAdded: (dispatcher: NavigationEventDispatcher) -> Unit = {},
+    private val onRemoved: () -> Unit = {},
+    private val onHasEnabledCallbacksChanged: (hasEnabledCallbacks: Boolean) -> Unit = {},
+) : NavigationEventInput() {
+
+    /** The number of times [onAdded] has been invoked. */
+    var addedInvocations: Int = 0
+        private set
+
+    /** The number of times [onRemoved] has been invoked. */
+    var removedInvocations: Int = 0
+        private set
+
+    /** The number of times [onHasEnabledCallbacksChanged] has been invoked. */
+    var onHasEnabledCallbacksChangedInvocations: Int = 0
+        private set
+
+    /**
+     * The most recently added [NavigationEventDispatcher].
+     *
+     * This is set by [onAdded] and cleared to `null` by [onRemoved].
+     */
+    var currentDispatcher: NavigationEventDispatcher? = null
+        private set
+
+    /**
+     * Test helper to simulate the start of a navigation event.
+     *
+     * This directly calls `dispatchOnStarted`, notifying any registered callbacks. Use this to
+     * trigger the beginning of a navigation flow in your tests.
+     *
+     * @param event The [NavigationEvent] to dispatch.
+     */
+    @MainThread
+    fun handleOnStarted(event: NavigationEvent = NavigationEvent()) {
+        dispatchOnStarted(event)
+    }
+
+    /**
+     * Test helper to simulate the progress of a navigation event.
+     *
+     * This directly calls `dispatchOnProgressed`, notifying any registered callbacks.
+     *
+     * @param event The [NavigationEvent] to dispatch.
+     */
+    @MainThread
+    fun handleOnProgressed(event: NavigationEvent = NavigationEvent()) {
+        dispatchOnProgressed(event)
+    }
+
+    /**
+     * Test helper to simulate the completion of a navigation event.
+     *
+     * This directly calls `dispatchOnCompleted`, notifying any registered callbacks.
+     */
+    @MainThread
+    fun handleOnCompleted() {
+        dispatchOnCompleted()
+    }
+
+    /**
+     * Test helper to simulate the cancellation of a navigation event.
+     *
+     * This directly calls `dispatchOnCancelled`, notifying any registered callbacks.
+     */
+    @MainThread
+    fun handleOnCancelled() {
+        dispatchOnCancelled()
+    }
+
+    override fun onAdded(dispatcher: NavigationEventDispatcher) {
+        addedInvocations++
+        currentDispatcher = dispatcher
+        onAdded.invoke(dispatcher)
+    }
+
+    override fun onRemoved() {
+        currentDispatcher = null
+        removedInvocations++
+        onRemoved.invoke()
+    }
+
+    override fun onHasEnabledCallbacksChanged(hasEnabledCallbacks: Boolean) {
+        onHasEnabledCallbacksChangedInvocations++
+        onHasEnabledCallbacksChanged.invoke(hasEnabledCallbacks)
     }
 }
diff --git a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventHierarchyTest.kt b/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventHierarchyTest.kt
deleted file mode 100644
index 111c186..0000000
--- a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/NavigationEventHierarchyTest.kt
+++ /dev/null
@@ -1,620 +0,0 @@
-/*
- * Copyright 2025 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.navigationevent
-
-import androidx.kruth.assertThat
-import androidx.kruth.assertThrows
-import androidx.navigationevent.testing.TestNavigationEventCallback
-import kotlin.test.Test
-
-class NavigationEventHierarchyTest {
-
-    @Test
-    fun init_whenChildIsCreatedWithParent_thenCallbacksAreSharedAndDispatched() {
-        // Given a parent dispatcher and a callback for it
-        val parentDispatcher = NavigationEventDispatcher()
-        val parentCallback = TestNavigationEventCallback()
-        parentDispatcher.addCallback(parentCallback)
-
-        // When a child dispatcher is created with the parent and a callback is added to the child
-        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
-        val childCallback = TestNavigationEventCallback()
-        childDispatcher.addCallback(childCallback)
-
-        // Then, dispatching an event from the parent should also trigger the child's callback,
-        // indicating the shared processing.
-        val event = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        parentDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(event)
-
-        assertThat(parentCallback.startedInvocations)
-            .isEqualTo(0) // Assuming LIFO, parent callback is skipped
-        assertThat(childCallback.startedInvocations).isEqualTo(1)
-    }
-
-    @Test
-    fun init_whenChildIsCreatedWithNoParent_thenCallbacksAreIndependent() {
-        // Given a parent dispatcher and a child dispatcher created without a parent
-        val parentDispatcher = NavigationEventDispatcher()
-        val childDispatcher = NavigationEventDispatcher()
-
-        // And a callback for each
-        val parentCallback = TestNavigationEventCallback()
-        val childCallback = TestNavigationEventCallback()
-        parentDispatcher.addCallback(parentCallback)
-        childDispatcher.addCallback(childCallback)
-
-        // When an event is dispatched through the parent
-        val event = NavigationEvent()
-        val parentInputHandler = DirectNavigationEventInputHandler()
-        parentDispatcher.addInputHandler(parentInputHandler)
-        parentInputHandler.handleOnStarted(event)
-
-        // Then only the parent's callback should be invoked, showing independent processing.
-        assertThat(parentCallback.startedInvocations).isEqualTo(1)
-        assertThat(childCallback.startedInvocations).isEqualTo(0)
-
-        // When an event is dispatched through the child
-        val childInputHandler = DirectNavigationEventInputHandler()
-        childDispatcher.addInputHandler(childInputHandler)
-        childInputHandler.handleOnStarted(event)
-
-        // Then only the child's callback should be invoked, showing independent processing.
-        assertThat(parentCallback.startedInvocations).isEqualTo(1)
-        assertThat(childCallback.startedInvocations).isEqualTo(1)
-    }
-
-    @Test
-    fun addCallback_whenCalledOnChild_thenCallbackIsDispatchedViaParent() {
-        // Given a parent and child dispatcher
-        val parentDispatcher = NavigationEventDispatcher()
-        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
-        val callback = TestNavigationEventCallback()
-
-        // When a new callback is added to the child
-        childDispatcher.addCallback(callback)
-
-        // Then dispatching an event from the parent should trigger the child's callback
-        val event = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        parentDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(event)
-        assertThat(callback.startedInvocations).isEqualTo(1)
-    }
-
-    @Test
-    fun addCallback_whenAddedToParentThenChild_thenCallbacksAreOrderedLIFO() {
-        // Given a parent and child dispatcher
-        val parentDispatcher = NavigationEventDispatcher()
-        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
-        val parentCallback = TestNavigationEventCallback()
-        val childCallback = TestNavigationEventCallback()
-
-        // When a callback is added to the parent, then to the child
-        parentDispatcher.addCallback(parentCallback)
-        childDispatcher.addCallback(childCallback)
-
-        // Then when an event is dispatched, the last-added callback (child's) should be invoked
-        // first.
-        val event = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        parentDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(event)
-
-        assertThat(parentCallback.startedInvocations).isEqualTo(0)
-        assertThat(childCallback.startedInvocations).isEqualTo(1)
-    }
-
-    @Test
-    fun addCallback_whenMultipleDispatchersAndCallbacksAdded_thenLastAddedCallbackIsInvokedFirst() {
-        // Given a parent NavigationEventDispatcher and two child dispatchers.
-        val parentDispatcher = NavigationEventDispatcher()
-        val child1Dispatcher = NavigationEventDispatcher(parentDispatcher)
-        val child2Dispatcher = NavigationEventDispatcher(parentDispatcher)
-
-        // And three TestNavigationCallbacks: one for the parent and one for each child.
-        val parentCallback = TestNavigationEventCallback()
-        val childCallback1 = TestNavigationEventCallback()
-        val childCallback2 = TestNavigationEventCallback()
-
-        // When callbacks are added to the parent, then child2, then child1.
-        parentDispatcher.addCallback(parentCallback)
-        child2Dispatcher.addCallback(childCallback2)
-        child1Dispatcher.addCallback(childCallback1)
-
-        // Then, when an event is dispatched through the parent, only the most recently added active
-        // callback (callbackC1 from child1) receives the event. This demonstrates that callbacks
-        // are processed in a LIFO manner across the dispatcher hierarchy and that subsequent
-        // callbacks are not invoked if an earlier one does not pass through.
-        val event = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        parentDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(event)
-
-        assertThat(parentCallback.startedInvocations).isEqualTo(0)
-        assertThat(childCallback2.startedInvocations).isEqualTo(0)
-        assertThat(childCallback1.startedInvocations).isEqualTo(1)
-    }
-
-    @Test
-    fun dispose_whenCalledOnChild_thenParentCallbackStillReceivesEvents() {
-        // Given a parent and child, both with callbacks
-        val parentDispatcher = NavigationEventDispatcher()
-        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
-        val parentCallback = TestNavigationEventCallback()
-        val childCallback = TestNavigationEventCallback()
-        parentDispatcher.addCallback(parentCallback)
-        childDispatcher.addCallback(childCallback)
-
-        // When child is disposed
-        childDispatcher.dispose()
-
-        // Then dispatching an event from the parent should only trigger the parent's callback
-        val event = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        parentDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(event)
-        assertThat(parentCallback.startedInvocations).isEqualTo(1)
-        assertThat(childCallback.startedInvocations).isEqualTo(0)
-    }
-
-    @Test
-    fun dispose_whenCalledOnParent_cascadesAndThrowsExceptionOnUse() {
-        // Given a parent and child dispatcher
-        val parentDispatcher = NavigationEventDispatcher()
-        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
-
-        // When the parent is disposed
-        parentDispatcher.dispose()
-
-        // Then attempting to use either dispatcher throws an exception
-        val event = NavigationEvent()
-        assertThrows<IllegalStateException> {
-                val inputHandler = DirectNavigationEventInputHandler()
-                parentDispatcher.addInputHandler(inputHandler)
-                inputHandler.handleOnStarted(event)
-            }
-            .hasMessageThat()
-            .contains("has already been disposed")
-        assertThrows<IllegalStateException> {
-                val inputHandler = DirectNavigationEventInputHandler()
-                childDispatcher.addInputHandler(inputHandler)
-                inputHandler.handleOnStarted(event)
-            }
-            .hasMessageThat()
-            .contains("has already been disposed")
-    }
-
-    @Test
-    fun dispose_whenCalledOnGrandparent_cascadesAndThrowsExceptionOnUse() {
-        // Given a three-level dispatcher hierarchy
-        val grandparentDispatcher = NavigationEventDispatcher()
-        val parentDispatcher = NavigationEventDispatcher(grandparentDispatcher)
-        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
-
-        // When the grandparent is disposed
-        grandparentDispatcher.dispose()
-
-        // Then attempting to use any dispatcher in the hierarchy throws an exception
-        val event = NavigationEvent()
-        assertThrows<IllegalStateException> {
-                val inputHandler = DirectNavigationEventInputHandler()
-                grandparentDispatcher.addInputHandler(inputHandler)
-                inputHandler.handleOnStarted(event)
-            }
-            .hasMessageThat()
-            .contains("has already been disposed")
-        assertThrows<IllegalStateException> {
-                val inputHandler = DirectNavigationEventInputHandler()
-                parentDispatcher.addInputHandler(inputHandler)
-                inputHandler.handleOnStarted(event)
-            }
-            .hasMessageThat()
-            .contains("has already been disposed")
-        assertThrows<IllegalStateException> {
-                val inputHandler = DirectNavigationEventInputHandler()
-                childDispatcher.addInputHandler(inputHandler)
-                inputHandler.handleOnStarted(event)
-            }
-            .hasMessageThat()
-            .contains("has already been disposed")
-    }
-
-    @Test
-    fun isEnabled_whenSetToTrue_thenEventsAreDispatched() {
-        // Given a dispatcher
-        val dispatcher = NavigationEventDispatcher()
-        val callback = TestNavigationEventCallback()
-        dispatcher.addCallback(callback)
-
-        // When the dispatcher is enabled
-        dispatcher.isEnabled = true
-
-        // Then dispatching an event should trigger the callback
-        val event = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(event)
-        assertThat(callback.startedInvocations).isEqualTo(1)
-    }
-
-    @Test
-    fun isEnabled_whenSetToFalse_thenNoCallbacksAreDispatched() {
-        // Given a dispatcher with an enabled callback
-        val dispatcher = NavigationEventDispatcher()
-        val callback = TestNavigationEventCallback(isEnabled = true)
-        dispatcher.addCallback(callback)
-
-        // When the dispatcher is disabled
-        dispatcher.isEnabled = false
-
-        // Then dispatching an event should not trigger the callback
-        val event = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(event)
-        assertThat(callback.startedInvocations).isEqualTo(0)
-    }
-
-    @Test
-    fun isEnabled_whenParentIsDisabled_thenChildDoesNotDispatchEvents() {
-        // Given a parent and child dispatcher, both with callbacks
-        val parentDispatcher = NavigationEventDispatcher()
-        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
-        val parentCallback = TestNavigationEventCallback()
-        val childCallback = TestNavigationEventCallback()
-        parentDispatcher.addCallback(parentCallback)
-        childDispatcher.addCallback(childCallback)
-
-        // When the parent is disabled
-        parentDispatcher.isEnabled = false
-
-        // Then dispatching an event from the child should not invoke any callbacks,
-        // because the parent's disabled state propagates.
-        val event = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        childDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(event)
-
-        assertThat(parentCallback.startedInvocations).isEqualTo(0)
-        assertThat(childCallback.startedInvocations).isEqualTo(0)
-    }
-
-    @Test
-    fun isEnabled_whenChildIsLocallyDisabled_thenChildDoesNotDispatchEvents() {
-        // Given a parent (enabled) and child, both with callbacks
-        val parentDispatcher = NavigationEventDispatcher()
-        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
-        val parentCallback = TestNavigationEventCallback()
-        val childCallback = TestNavigationEventCallback()
-        parentDispatcher.addCallback(parentCallback)
-        childDispatcher.addCallback(childCallback)
-
-        // When child is locally disabled
-        childDispatcher.isEnabled = false
-
-        // Then dispatching an event from the child should not trigger its callback.
-        // The parent's callback should still be invokable via the parent directly.
-        val event = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        childDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(event)
-
-        assertThat(childCallback.startedInvocations).isEqualTo(0)
-        assertThat(parentCallback.startedInvocations)
-            .isEqualTo(0) // Parent's callback should still fire via parent
-    }
-
-    @Test
-    fun isEnabled_whenChildIsLocallyDisabled_thenChildCallbacksDoesNotReceiveEvents() {
-        // Given a parent (enabled) and child, both with callbacks
-        val parentDispatcher = NavigationEventDispatcher()
-        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
-        val parentCallback = TestNavigationEventCallback()
-        val childCallback = TestNavigationEventCallback()
-        parentDispatcher.addCallback(parentCallback)
-        childDispatcher.addCallback(childCallback)
-
-        // When child is locally disabled
-        childDispatcher.isEnabled = false
-
-        // Then dispatching an event from the child should not trigger its callback.
-        // The parent's callback should still be invokable via the parent directly.
-        val event = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        parentDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(event) // Confirm parent is still active
-
-        assertThat(childCallback.startedInvocations).isEqualTo(0)
-        assertThat(parentCallback.startedInvocations)
-            .isEqualTo(1) // Parent's callback should still fire via parent
-    }
-
-    @Test
-    fun isEnabled_whenDisabledParentIsReEnabled_thenChildDispatchesEventsAgain() {
-        // Given a disabled parent and a locally-enabled child, both with callbacks
-        val parentDispatcher = NavigationEventDispatcher()
-        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
-        val parentCallback = TestNavigationEventCallback()
-        val childCallback = TestNavigationEventCallback()
-        parentDispatcher.addCallback(parentCallback)
-        childDispatcher.addCallback(childCallback)
-
-        parentDispatcher.isEnabled = false // Initial state: parent (and thus child) disabled
-        // Verify pre-condition (no dispatch before re-enabling)
-        val initialEvent = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        childDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(initialEvent)
-        assertThat(childCallback.startedInvocations).isEqualTo(0)
-
-        // When the parent is re-enabled
-        parentDispatcher.isEnabled = true
-
-        // Then the child should now dispatch events
-        val reEnabledEvent = NavigationEvent()
-        inputHandler.handleOnStarted(reEnabledEvent)
-
-        assertThat(childCallback.startedInvocations).isEqualTo(1)
-        assertThat(parentCallback.startedInvocations)
-            .isEqualTo(0) // Parent's callback is still LIFO behind child
-    }
-
-    @Test
-    fun isEnabled_whenDisabledParentIsReEnabled_thenChildCallbacksReceiveEventsAgain() {
-        // Given a disabled parent and a locally-enabled child, both with callbacks
-        val parentDispatcher = NavigationEventDispatcher()
-        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
-        val parentCallback = TestNavigationEventCallback()
-        val childCallback = TestNavigationEventCallback()
-        parentDispatcher.addCallback(parentCallback)
-        childDispatcher.addCallback(childCallback)
-
-        parentDispatcher.isEnabled = false // Initial state: parent (and thus child) disabled
-        // Verify pre-condition (no dispatch before re-enabling)
-        val initialEvent = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        parentDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(initialEvent)
-        assertThat(parentCallback.startedInvocations).isEqualTo(0)
-        assertThat(childCallback.startedInvocations).isEqualTo(0)
-
-        // When the parent is re-enabled
-        parentDispatcher.isEnabled = true
-
-        // Then the child should now dispatch events
-        val reEnabledEvent = NavigationEvent()
-        inputHandler.handleOnStarted(reEnabledEvent)
-        assertThat(parentCallback.startedInvocations)
-            .isEqualTo(0) // Parent's callback is still LIFO behind child
-        assertThat(childCallback.startedInvocations).isEqualTo(1)
-    }
-
-    @Test
-    fun isEnabled_whenGrandparentIsDisabled_thenGrandchildDoesNotDispatchEvents() {
-        // Given a three-level hierarchy, each with a callback
-        val grandparentDispatcher = NavigationEventDispatcher()
-        val parentDispatcher = NavigationEventDispatcher(grandparentDispatcher)
-        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
-        val grandparentCallback = TestNavigationEventCallback()
-        val parentCallback = TestNavigationEventCallback()
-        val childCallback = TestNavigationEventCallback()
-
-        grandparentDispatcher.addCallback(grandparentCallback)
-        parentDispatcher.addCallback(parentCallback)
-        childDispatcher.addCallback(childCallback)
-
-        // When the grandparent is disabled
-        grandparentDispatcher.isEnabled = false
-
-        // Then dispatching an event from the grandchild should result in no callbacks being
-        // invoked, as the disabled state cascades down.
-        val event = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        childDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(event)
-
-        assertThat(grandparentCallback.startedInvocations).isEqualTo(0)
-        assertThat(parentCallback.startedInvocations).isEqualTo(0)
-        assertThat(childCallback.startedInvocations).isEqualTo(0)
-    }
-
-    @Test
-    fun isEnabled_whenGrandparentIsDisabled_thenGrandchildCallbackDoesNotReceiveEvents() {
-        // Given a three-level hierarchy, each with a callback
-        val grandparentDispatcher = NavigationEventDispatcher()
-        val parentDispatcher = NavigationEventDispatcher(grandparentDispatcher)
-        val childDispatcher = NavigationEventDispatcher(parentDispatcher)
-        val grandparentCallback = TestNavigationEventCallback()
-        val parentCallback = TestNavigationEventCallback()
-        val childCallback = TestNavigationEventCallback()
-
-        grandparentDispatcher.addCallback(grandparentCallback)
-        parentDispatcher.addCallback(parentCallback)
-        childDispatcher.addCallback(childCallback)
-
-        // When the grandparent is disabled
-        grandparentDispatcher.isEnabled = false
-
-        // Then dispatching an event from the grandparent should result in no callbacks being
-        // invoked, as the disabled state cascades down.
-        val event = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        grandparentDispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(event)
-
-        assertThat(grandparentCallback.startedInvocations).isEqualTo(0)
-        assertThat(parentCallback.startedInvocations).isEqualTo(0)
-        assertThat(childCallback.startedInvocations).isEqualTo(0)
-    }
-
-    @Test
-    fun callbackIsEnabled_whenItsDispatcherIsDisabled_thenCallbackDoesNotReceiveEvents() {
-        // Given a dispatcher and an enabled TestNavigationCallback
-        val dispatcher = NavigationEventDispatcher()
-        val callback = TestNavigationEventCallback()
-        dispatcher.addCallback(callback)
-        // Ensure callback is initially enabled
-        val preDisableEvent = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(preDisableEvent)
-        assertThat(callback.startedInvocations).isEqualTo(1)
-
-        // When the dispatcher associated with the callback is disabled
-        dispatcher.isEnabled = false
-
-        // Then dispatching an event (even if the callback's local isEnabled is true)
-        // should not trigger the callback because its dispatcher is disabled.
-        val event = NavigationEvent()
-        inputHandler.handleOnStarted(event)
-
-        assertThat(callback.startedInvocations).isEqualTo(1)
-    }
-
-    @Test
-    fun callbackIsEnabled_whenItsDispatcherIsReEnabled_thenCallbackReceivesEventsAgain() {
-        // Given a dispatcher and an enabled TestNavigationCallback, and the dispatcher is disabled
-        val dispatcher = NavigationEventDispatcher()
-        val callback = TestNavigationEventCallback()
-        dispatcher.addCallback(callback)
-        dispatcher.isEnabled = false // Disable dispatcher
-
-        // Pre-condition: Callback does not receive events when dispatcher is disabled
-        val preEnableEvent = NavigationEvent()
-        val inputHandler = DirectNavigationEventInputHandler()
-        dispatcher.addInputHandler(inputHandler)
-        inputHandler.handleOnStarted(preEnableEvent)
-        assertThat(callback.startedInvocations).isEqualTo(0)
-
-        // When the dispatcher associated with the callback is re-enabled
-        dispatcher.isEnabled = true
-
-        // Then dispatching an event should now trigger the callback
-        val reEnabledEvent = NavigationEvent()
-        inputHandler.handleOnStarted(reEnabledEvent)
-
-        assertThat(callback.startedInvocations).isEqualTo(1)
-    }
-
-    @Test
-    fun addCallback_onDisposedDispatcher_throwsException() {
-        val dispatcher = NavigationEventDispatcher()
-        dispatcher.dispose()
-
-        // Adding a callback to a disposed dispatcher should fail.
-        assertThrows<IllegalStateException> {
-                dispatcher.addCallback(TestNavigationEventCallback())
-            }
-            .hasMessageThat()
-            .contains("has already been disposed")
-    }
-
-    @Test
-    fun handleOnStarted_onDisposedDispatcher_throwsException() {
-        val dispatcher = NavigationEventDispatcher()
-        dispatcher.dispose()
-
-        // Dispatching on a disposed dispatcher should fail.
-        assertThrows<IllegalStateException> {
-                val inputHandler = DirectNavigationEventInputHandler()
-                dispatcher.addInputHandler(inputHandler)
-                inputHandler.handleOnStarted(NavigationEvent())
-            }
-            .hasMessageThat()
-            .contains("has already been disposed")
-    }
-
-    @Test
-    fun handleOnProgressed_onDisposedDispatcher_throwsException() {
-        val dispatcher = NavigationEventDispatcher()
-        dispatcher.dispose()
-
-        // Dispatching on a disposed dispatcher should fail.
-        assertThrows<IllegalStateException> {
-                val inputHandler = DirectNavigationEventInputHandler()
-                dispatcher.addInputHandler(inputHandler)
-                inputHandler.handleOnProgressed(NavigationEvent())
-            }
-            .hasMessageThat()
-            .contains("has already been disposed")
-    }
-
-    @Test
-    fun handleOnCompleted_onDisposedDispatcher_throwsException() {
-        val dispatcher = NavigationEventDispatcher()
-        dispatcher.dispose()
-
-        // Dispatching on a disposed dispatcher should fail.
-        assertThrows<IllegalStateException> {
-                val inputHandler = DirectNavigationEventInputHandler()
-                dispatcher.addInputHandler(inputHandler)
-                inputHandler.handleOnCompleted()
-            }
-            .hasMessageThat()
-            .contains("has already been disposed")
-    }
-
-    @Test
-    fun handleOnCancelled_onDisposedDispatcher_throwsException() {
-        val dispatcher = NavigationEventDispatcher()
-        dispatcher.dispose()
-
-        // Dispatching on a disposed dispatcher should fail.
-        assertThrows<IllegalStateException> {
-                val inputHandler = DirectNavigationEventInputHandler()
-                dispatcher.addInputHandler(inputHandler)
-                inputHandler.handleOnCancelled()
-            }
-            .hasMessageThat()
-            .contains("has already been disposed")
-    }
-
-    @Test
-    fun dispose_onDisposedDispatcher_throwsException() {
-        val dispatcher = NavigationEventDispatcher()
-        dispatcher.dispose() // First disposal is fine.
-
-        // Disposing an already-disposed dispatcher should fail.
-        assertThrows<IllegalStateException> { dispatcher.dispose() }
-            .hasMessageThat()
-            .contains("has already been disposed")
-    }
-
-    @Test
-    fun dispose_enabled_throwsException() {
-        val dispatcher = NavigationEventDispatcher()
-        dispatcher.dispose() // First disposal is fine.
-
-        // Enabling an already-disposed dispatcher should fail.
-        assertThrows<IllegalStateException> { dispatcher.isEnabled = false }
-            .hasMessageThat()
-            .contains("has already been disposed")
-    }
-
-    @Test
-    fun dispose_disabled_throwsException() {
-        val dispatcher = NavigationEventDispatcher()
-        dispatcher.dispose() // First disposal is fine.
-
-        // disabling an already-disposed dispatcher should fail.
-        assertThrows<IllegalStateException> { dispatcher.isEnabled = false }
-            .hasMessageThat()
-            .contains("has already been disposed")
-    }
-}
diff --git a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/internal/TestNavigationEventInput.kt b/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/internal/TestNavigationEventInput.kt
deleted file mode 100644
index c2fbdca..0000000
--- a/navigationevent/navigationevent/src/commonTest/kotlin/androidx/navigationevent/internal/TestNavigationEventInput.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright 2025 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.navigationevent.internal
-
-import androidx.annotation.MainThread
-import androidx.navigationevent.NavigationEvent
-import androidx.navigationevent.NavigationEventDispatcher
-import androidx.navigationevent.NavigationEventInputHandler
-
-/**
- * A test implementation of [NavigationEventInputHandler] that records lifecycle events and
- * invocation counts.
- *
- * Use this class in tests to verify that `onAdded`, `onRemoved`, and `onHasEnabledCallbacksChanged`
- * are called correctly. It counts how many times each lifecycle method is invoked and stores a
- * reference to the most recently added dispatcher. It also provides helper methods to simulate
- * dispatching navigation events.
- *
- * @param onAdded An optional lambda to execute when [onAdded] is called.
- * @param onRemoved An optional lambda to execute when [onRemoved] is called.
- * @param onHasEnabledCallbacksChanged An optional lambda to execute when
- *   [onHasEnabledCallbacksChanged] is called.
- */
-// TODO(mgalhardo): aosp/3732271 is renaming this to `Input`
-internal class TestNavigationEventInput(
-    private val onAdded: (dispatcher: NavigationEventDispatcher) -> Unit = {},
-    private val onRemoved: () -> Unit = {},
-    private val onHasEnabledCallbacksChanged: (hasEnabledCallbacks: Boolean) -> Unit = {},
-) : NavigationEventInputHandler() {
-
-    /** The number of times [onAdded] has been invoked. */
-    var addedInvocations: Int = 0
-        private set
-
-    /** The number of times [onRemoved] has been invoked. */
-    var removedInvocations: Int = 0
-        private set
-
-    /** The number of times [onHasEnabledCallbacksChanged] has been invoked. */
-    var onHasEnabledCallbacksChangedInvocations: Int = 0
-        private set
-
-    /**
-     * The most recently added [NavigationEventDispatcher].
-     *
-     * This is set by [onAdded] and cleared to `null` by [onRemoved].
-     */
-    var currentDispatcher: NavigationEventDispatcher? = null
-        private set
-
-    /**
-     * Test helper to simulate the start of a navigation event.
-     *
-     * This directly calls `dispatchOnStarted`, notifying any registered callbacks. Use this to
-     * trigger the beginning of a navigation flow in your tests.
-     *
-     * @param event The [NavigationEvent] to dispatch.
-     */
-    @MainThread
-    fun handleOnStarted(event: NavigationEvent = NavigationEvent()) {
-        dispatchOnStarted(event)
-    }
-
-    /**
-     * Test helper to simulate the progress of a navigation event.
-     *
-     * This directly calls `dispatchOnProgressed`, notifying any registered callbacks.
-     *
-     * @param event The [NavigationEvent] to dispatch.
-     */
-    @MainThread
-    fun handleOnProgressed(event: NavigationEvent = NavigationEvent()) {
-        dispatchOnProgressed(event)
-    }
-
-    /**
-     * Test helper to simulate the completion of a navigation event.
-     *
-     * This directly calls `dispatchOnCompleted`, notifying any registered callbacks.
-     */
-    @MainThread
-    fun handleOnCompleted() {
-        dispatchOnCompleted()
-    }
-
-    /**
-     * Test helper to simulate the cancellation of a navigation event.
-     *
-     * This directly calls `dispatchOnCancelled`, notifying any registered callbacks.
-     */
-    @MainThread
-    fun handleOnCancelled() {
-        dispatchOnCancelled()
-    }
-
-    // TODO(mgalhardo): aosp/3732466 is renaming this to `onAdded`
-    override fun onAttach(dispatcher: NavigationEventDispatcher) {
-        addedInvocations++
-        currentDispatcher = dispatcher
-        onAdded.invoke(dispatcher)
-    }
-
-    // TODO(mgalhardo): aosp/3732466 is renaming this to `onRemoved`
-    override fun onDetach() {
-        currentDispatcher = null
-        removedInvocations++
-        onRemoved.invoke()
-    }
-
-    override fun onHasEnabledCallbacksChanged(hasEnabledCallbacks: Boolean) {
-        onHasEnabledCallbacksChangedInvocations++
-        onHasEnabledCallbacksChanged.invoke(hasEnabledCallbacks)
-    }
-}
diff --git a/paging/paging-common/build.gradle b/paging/paging-common/build.gradle
index c135920..97bc2218d 100644
--- a/paging/paging-common/build.gradle
+++ b/paging/paging-common/build.gradle
@@ -119,36 +119,6 @@
             dependsOn(commonTest)
         }
 
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
-                // https://youtrack.jetbrains.com/issue/KT-71032
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        jsTest {
-            dependsOn(webTest)
-        }
-
-        wasmJsMain {
-            dependsOn(webMain)
-            dependencies {
-                // For KotlinWasm/Js, versions of toolchain and stdlib need to be the same (2.1):
-                // https://youtrack.jetbrains.com/issue/KT-71032
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        wasmJsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-                implementation(libs.kotlinTestForWasmTests)
-            }
-        }
-
         jvmTest {
             dependsOn(commonJvmAndroidTest)
         }
@@ -176,6 +146,9 @@
                         throw new GradleException("unknown native target ${target}")
                     }
                 }
+            } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
+                target.compilations["main"].defaultSourceSet.dependsOn(webMain)
+                target.compilations["test"].defaultSourceSet.dependsOn(webTest)
             }
         }
     }
diff --git a/room/room-common/build.gradle b/room/room-common/build.gradle
index 1671885..eabf42c 100644
--- a/room/room-common/build.gradle
+++ b/room/room-common/build.gradle
@@ -55,8 +55,7 @@
         commonTest {
             dependencies {
                 implementation(project(":kruth:kruth"))
-                // https://youtrack.jetbrains.com/issue/KT-71032
-                implementation(libs.kotlinTestForWasmTests)
+                implementation(libs.kotlinTest)
             }
         }
 
@@ -76,25 +75,14 @@
             dependsOn(commonMain)
         }
 
-        jsMain {
-            dependsOn(commonMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        wasmJsMain {
-            dependsOn(commonMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
+        nativeTest {
+            dependsOn(commonTest)
         }
 
         targets.configureEach { target ->
             if (target.platformType == KotlinPlatformType.native) {
-                target.compilations["main"].defaultSourceSet {
-                    dependsOn(nativeMain)
-                }
+                target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
+                target.compilations["test"].defaultSourceSet.dependsOn(nativeTest)
             }
         }
     }
diff --git a/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt b/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
index c1a53a0..5612081 100644
--- a/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
+++ b/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
@@ -38,7 +38,6 @@
 import androidx.testutils.TestExecutor
 import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
-import kotlin.test.Ignore
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
 import kotlinx.coroutines.CoroutineName
@@ -735,9 +734,9 @@
         db.close()
     }
 
-    @Ignore("Due to b/373727432.")
     @Test
     fun invalid_append() = runTest {
+        dao.addAllItems(ITEMS_LIST)
         val pagingSource = LimitOffsetPagingSourceImpl(db)
         val pager = TestPager(CONFIG, pagingSource)
 
@@ -763,9 +762,9 @@
         assertThat(pagingSource.invalid).isTrue()
     }
 
-    @Ignore("Due to b/365167269.")
     @Test
     fun invalid_prepend() = runTest {
+        dao.addAllItems(ITEMS_LIST)
         val pagingSource = LimitOffsetPagingSourceImpl(db)
         val pager = TestPager(CONFIG, pagingSource)
 
diff --git a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
index d958dbd..701b58b 100644
--- a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
+++ b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
@@ -73,35 +73,28 @@
 
     internal val itemCount = AtomicInt(INITIAL_ITEM_COUNT)
 
-    private val flow = db.invalidationTracker.createFlow(*tables, emitInitialState = false)
-
-    private val invalidationFlowStarted = AtomicBoolean(false)
-
     private val refreshComplete = AtomicBoolean(false)
 
     private var invalidationFlowJob: Job? = null
 
     init {
         // register db listeners right away
-        db.getCoroutineScope().launch { flow.collect() }
+        invalidationFlowJob =
+            db.getCoroutineScope().launch {
+                db.invalidationTracker.createFlow(*tables, emitInitialState = false).collect {
+                    if (pagingSource.invalid) {
+                        throw CancellationException("PagingSource is invalid")
+                    }
+                    if (refreshComplete.get()) {
+                        pagingSource.invalidate()
+                    }
+                }
+            }
 
         pagingSource.registerInvalidatedCallback { invalidationFlowJob?.cancel() }
     }
 
     suspend fun load(params: LoadParams<Int>): LoadResult<Int, Value> {
-        if (invalidationFlowStarted.compareAndSet(false, true)) {
-            invalidationFlowJob =
-                db.getCoroutineScope().launch {
-                    db.invalidationTracker.createFlow(*tables, emitInitialState = false).collect {
-                        if (pagingSource.invalid) {
-                            throw CancellationException("PagingSource is invalid")
-                        }
-                        if (refreshComplete.get()) {
-                            pagingSource.invalidate()
-                        }
-                    }
-                }
-        }
         val tempCount = itemCount.get()
         // if itemCount is < 0, then it is initial load
         return try {
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/support/PrePackagedCopyOpenHelper.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/support/PrePackagedCopyOpenHelper.android.kt
index 97bf39b..49863d4 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/support/PrePackagedCopyOpenHelper.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/support/PrePackagedCopyOpenHelper.android.kt
@@ -118,6 +118,16 @@
             if (currentVersion == databaseVersion) {
                 return
             }
+            if (
+                databaseConfiguration.migrationContainer.findMigrationPath(
+                    currentVersion,
+                    databaseVersion,
+                ) != null
+            ) {
+                // There is a migration path and it will be prioritized,  i.e. we won't be
+                // performing a copy destructive migration.
+                return
+            }
             if (databaseConfiguration.isMigrationRequired(currentVersion, databaseVersion)) {
                 // From the current version to the desired version a migration is required, i.e.
                 // we won't be performing a copy destructive migration.
diff --git a/savedstate/savedstate-compose/build.gradle b/savedstate/savedstate-compose/build.gradle
index b25aca2..aa19acc 100644
--- a/savedstate/savedstate-compose/build.gradle
+++ b/savedstate/savedstate-compose/build.gradle
@@ -103,36 +103,6 @@
             dependsOn(nonAndroidTest)
         }
 
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        jsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-                implementation(libs.kotlinTestJs)
-            }
-        }
-
-        wasmJsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        wasmJsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-                implementation(libs.kotlinTestWasm)
-            }
-        }
-
         targets.configureEach { target ->
             if (target.platformType == KotlinPlatformType.native) {
                 target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
diff --git a/savedstate/savedstate-testing/build.gradle b/savedstate/savedstate-testing/build.gradle
index ffe49bf..66d47d2 100644
--- a/savedstate/savedstate-testing/build.gradle
+++ b/savedstate/savedstate-testing/build.gradle
@@ -25,6 +25,7 @@
 import androidx.build.PlatformIdentifier
 import androidx.build.SoftwareType
 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
+import org.jetbrains.kotlin.konan.target.Family
 
 plugins {
     id("AndroidXPlugin")
@@ -165,45 +166,15 @@
             }
         }
 
-        wasmJsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        wasmJsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-                implementation(libs.kotlinTestForWasmTests)
-            }
-        }
-
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        jsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-                implementation(libs.kotlinTestJs)
-            }
-        }
-
         targets.configureEach { target ->
             if (target.platformType == KotlinPlatformType.native) {
                 if (target.konanTarget.family.appleFamily) {
                     target.compilations["main"].defaultSourceSet.dependsOn(darwinMain)
                     target.compilations["test"].defaultSourceSet.dependsOn(darwinTest)
-                } else if (target.konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX) {
+                } else if (target.konanTarget.family == Family.LINUX) {
                     target.compilations["main"].defaultSourceSet.dependsOn(linuxMain)
                     target.compilations["test"].defaultSourceSet.dependsOn(linuxTest)
-                } else if (target.konanTarget.family == org.jetbrains.kotlin.konan.target.Family.MINGW) {
+                } else if (target.konanTarget.family == Family.MINGW) {
                     target.compilations["main"].defaultSourceSet.dependsOn(mingwMain)
                     target.compilations["test"].defaultSourceSet.dependsOn(mingwTest)
                 } else {
diff --git a/savedstate/savedstate/build.gradle b/savedstate/savedstate/build.gradle
index bd6baba..0df19ff 100644
--- a/savedstate/savedstate/build.gradle
+++ b/savedstate/savedstate/build.gradle
@@ -168,36 +168,6 @@
             }
         }
 
-        wasmJsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        wasmJsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-                implementation(libs.kotlinTestForWasmTests)
-            }
-        }
-
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        jsTest {
-            dependsOn(webTest)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-                implementation(libs.kotlinTestJs)
-            }
-        }
-
         targets.configureEach { target ->
             if (target.platformType == KotlinPlatformType.native) {
                 if (target.konanTarget.family.appleFamily) {
diff --git a/settings.gradle b/settings.gradle
index 4f45dba..cef59e5 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -603,6 +603,7 @@
 includeProject(":compose:remote:remote-creation-core", [BuildType.MAIN])
 includeProject(":compose:remote:remote-creation", [BuildType.MAIN])
 includeProject(":compose:remote:remote-player-view", [BuildType.MAIN])
+includeProject(":compose:remote:remote-player-compose", [BuildType.MAIN])
 includeProject(":compose:remote:remote-frontend", [BuildType.MAIN])
 includeProject(":compose:remote:remote-foundation", [BuildType.MAIN])
 includeProject(":compose:remote:test-utils", [BuildType.MAIN])
diff --git a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt
index 7b4ec59..daed86e 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/main/java/androidx/stableaidl/StableAidlPlugin.kt
@@ -102,7 +102,6 @@
             // we'll need to use manually-defined stubs.
             val compileSdkProvider =
                 project.provider {
-                    @Suppress("deprecation") // TODO(aurimas): migrate to new API
                     project.extensions.findByType(CommonExtension::class.java)?.compileSdk
                         ?: project.extensions
                             .findByType(KotlinMultiplatformExtension::class.java)
diff --git a/test/uiautomator/integration-tests/testapp/build.gradle b/test/uiautomator/integration-tests/testapp/build.gradle
index 31516f5..93f26af 100644
--- a/test/uiautomator/integration-tests/testapp/build.gradle
+++ b/test/uiautomator/integration-tests/testapp/build.gradle
@@ -47,7 +47,7 @@
 
     // Align dependencies in debugRuntimeClasspath and debugAndroidTestRuntimeClasspath.
 
-    androidTestImplementation("androidx.lifecycle:lifecycle-common:2.9.0")
+    androidTestImplementation(project(":lifecycle:lifecycle-common"))
     androidTestImplementation(libs.junit)
     androidTestImplementation(libs.testMonitor)
 }
diff --git a/testutils/testutils-lifecycle/build.gradle b/testutils/testutils-lifecycle/build.gradle
index b551d74..9f85c43 100644
--- a/testutils/testutils-lifecycle/build.gradle
+++ b/testutils/testutils-lifecycle/build.gradle
@@ -21,8 +21,10 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
+
 import androidx.build.SoftwareType
 import androidx.build.PlatformIdentifier
+import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
 
 plugins {
     id("AndroidXPlugin")
@@ -73,23 +75,11 @@
             dependsOn(commonMain)
         }
 
-        wasmJsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        jsMain {
-            dependsOn(webMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
         targets.all { target ->
-            if (target.platformType == org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.native) {
+            if (target.platformType == KotlinPlatformType.native) {
                 target.compilations["main"].defaultSourceSet.dependsOn(nativeMain)
+            } else if (target.platformType in [KotlinPlatformType.js, KotlinPlatformType.wasm]) {
+                target.compilations["main"].defaultSourceSet.dependsOn(webMain)
             }
         }
     }
diff --git a/testutils/testutils-paging/build.gradle b/testutils/testutils-paging/build.gradle
index 82e9973..1f1adc7 100644
--- a/testutils/testutils-paging/build.gradle
+++ b/testutils/testutils-paging/build.gradle
@@ -46,20 +46,6 @@
                 implementation(libs.kotlinTest)
             }
         }
-
-        jsMain {
-            dependsOn(commonMain)
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        wasmJsMain {
-            dependsOn(commonMain)
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
     }
 }
 
diff --git a/wear/compose/integration-tests/navigation/build.gradle b/wear/compose/integration-tests/navigation/build.gradle
index df65ccb..46a09c2 100644
--- a/wear/compose/integration-tests/navigation/build.gradle
+++ b/wear/compose/integration-tests/navigation/build.gradle
@@ -59,7 +59,7 @@
 
     // Align dependencies in debugRuntimeClasspath and debugAndroidTestRuntimeClasspath.
 
-    androidTestImplementation("androidx.lifecycle:lifecycle-common:2.9.0")
+    androidTestImplementation(project(":lifecycle:lifecycle-common"))
 }
 
 androidx {
diff --git a/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/WebViewBuilderTest.java b/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/WebViewBuilderTest.java
index 70e984e..853e3b5 100644
--- a/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/WebViewBuilderTest.java
+++ b/webkit/integration-tests/instrumentation/src/androidTest/java/androidx/webkit/WebViewBuilderTest.java
@@ -53,7 +53,7 @@
 
     @Test
     public void testConstructsWebView() {
-        WebViewBuilder builder = new WebViewBuilder();
+        WebViewBuilder builder = new WebViewBuilder(WebViewBuilder.Baseline.LEGACY);
 
         try (ActivityScenario<WebViewTestActivity> scenario =
                 ActivityScenario.launch(WebViewTestActivity.class)) {
@@ -75,7 +75,7 @@
 
     @Test
     public void testConstructsWebViewTwice() {
-        WebViewBuilder builder = new WebViewBuilder();
+        WebViewBuilder builder = new WebViewBuilder(WebViewBuilder.Baseline.LEGACY);
 
         try (ActivityScenario<WebViewTestActivity> scenario =
                 ActivityScenario.launch(WebViewTestActivity.class)) {
@@ -137,8 +137,9 @@
                             .javascriptInterface(new TestInterface(3), "jsInterface3")
                             .build();
 
-            WebViewBuilder builder =
-                    new WebViewBuilder().restrictJavascriptInterface().addAllowlist(allowlist);
+            WebViewBuilder builder = new WebViewBuilder(WebViewBuilder.Baseline.LEGACY)
+                    .restrictJavascriptInterface()
+                    .addAllowlist(allowlist);
 
             WebView webview = build(builder);
 
@@ -168,7 +169,8 @@
                         .javascriptInterface(jsInterface, "jsInterface")
                         .build();
 
-        WebViewBuilder builder = new WebViewBuilder().addAllowlist(allowlist);
+        WebViewBuilder builder = new WebViewBuilder(WebViewBuilder.Baseline.LEGACY)
+                .addAllowlist(allowlist);
 
         // This builder did not call restrictJavascriptInterface before allowlisting
         Assert.assertThrows(WebViewBuilderException.class, () -> build(builder));
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewBuilder.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewBuilder.java
index 9c73f26..b5bb634 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewBuilder.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewBuilder.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.webkit.WebView;
 
+import androidx.annotation.IntDef;
 import androidx.annotation.RequiresFeature;
 import androidx.annotation.RequiresOptIn;
 import androidx.annotation.RestrictTo;
@@ -59,9 +60,29 @@
     @RequiresOptIn(level = RequiresOptIn.Level.ERROR)
     public @interface Experimental {}
 
+    /**
+     * Common configuration presets for WebView.
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({
+        Baseline.LEGACY,
+    })
+    public @interface Baseline {
+        /**
+         * Matches the configuration of a WebView created via the WebView constructor.
+         */
+        int LEGACY = 0;
+    }
+
     @Nullable WebViewBuilderBoundaryInterface mBuilderStateBoundary;
 
-    public WebViewBuilder() {}
+    public WebViewBuilder(@Baseline int baseline) {
+        if (baseline != Baseline.LEGACY) {
+            throw new IllegalArgumentException("Invalid baseline: " + baseline);
+        }
+        // TODO(crbug.com/419726203): We only have the no-op LEGACY baseline right now, so no logic
+        // consumes this argument, yet.
+    }
 
     /**
      * Restrict {@link WebView#addJavascriptInterface(Object, String)} and {@link
diff --git a/window/window-core/build.gradle b/window/window-core/build.gradle
index 7da0f71..0ad5c2e 100644
--- a/window/window-core/build.gradle
+++ b/window/window-core/build.gradle
@@ -55,8 +55,7 @@
 
         commonTest {
             dependencies {
-                // https://youtrack.jetbrains.com/issue/KT-71032
-                implementation(libs.kotlinTestForWasmTests)
+                implementation(libs.kotlinTest)
                 implementation(libs.kotlinTestAnnotationsCommon)
                 implementation(libs.kotlinCoroutinesCore)
             }
@@ -67,32 +66,6 @@
                 implementation(libs.testRunner)
             }
         }
-
-        jsMain {
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-            }
-        }
-
-        jsTest {
-            dependencies {
-                implementation(libs.kotlinStdlibJs)
-                implementation(libs.kotlinTestJs)
-            }
-        }
-
-        wasmJsMain {
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-            }
-        }
-
-        wasmJsTest {
-            dependencies {
-                implementation(libs.kotlinStdlibWasm)
-                implementation(libs.kotlinTestWasm)
-            }
-        }
     }
 }
 
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 084179f..c562ade 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -57,7 +57,7 @@
     implementation(project(":window:window-core"))
 
     def extensions_core_version = "androidx.window.extensions.core:core:1.0.0"
-    def extensions_version = "androidx.window.extensions:extensions:1.5.0-beta01"
+    def extensions_version = "androidx.window.extensions:extensions:1.5.0"
     // A compile only dependency on extensions.core so that other libraries do not expose it
     // transitively.
     compileOnly(extensions_core_version)
diff --git a/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt b/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt
index d2db738..3d85d3e 100644
--- a/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SafeActivityEmbeddingComponentProvider.kt
@@ -27,7 +27,6 @@
 import androidx.window.SafeWindowExtensionsProvider
 import androidx.window.WindowSdkExtensions
 import androidx.window.core.ConsumerAdapter
-import androidx.window.embedding.EmbeddingConfiguration.DimAreaBehavior
 import androidx.window.extensions.WindowExtensions
 import androidx.window.extensions.core.util.function.Consumer
 import androidx.window.extensions.core.util.function.Function
@@ -210,7 +209,6 @@
      * - [ActivityStack.Token]
      * - [WindowAttributes]
      * - [SplitInfo.Token]
-     * - [EmbeddingConfiguration.Builder.setDimAreaBehavior]
      */
     @VisibleForTesting
     internal fun hasValidVendorApiLevel5(): Boolean =
@@ -224,8 +222,7 @@
             isClassAnimationBackgroundValid() &&
             isClassActivityStackTokenValid() &&
             isClassWindowAttributesValid() &&
-            isClassSplitInfoTokenValid() &&
-            isClassEmbeddingConfigurationBuilderApi5Valid()
+            isClassSplitInfoTokenValid()
 
     /**
      * Vendor API level 6 includes the following methods:
@@ -862,20 +859,6 @@
             createFromBinder.isPublic && createFromBinder.doesReturn(splitInfoTokenClass)
         }
 
-    private fun isClassEmbeddingConfigurationBuilderApi5Valid(): Boolean =
-        validateReflection("Class EmbeddingConfiguration.Builder is not valid") {
-            val EmbeddingConfigurationBuilderClass = EmbeddingConfiguration.Builder::class.java
-            val setAutoSaveEmbeddingStateMethod =
-                EmbeddingConfigurationBuilderClass.getMethod(
-                    "setDimAreaBehavior",
-                    DimAreaBehavior::class.java,
-                )
-            setAutoSaveEmbeddingStateMethod.isPublic &&
-                setAutoSaveEmbeddingStateMethod.doesReturn(
-                    EmbeddingConfiguration.Builder::class.java
-                )
-        }
-
     /** Vendor API level 6 validation methods */
     private fun isMethodGetEmbeddedActivityWindowInfoValid(): Boolean =
         validateReflection(
@@ -1090,21 +1073,6 @@
                 setChangeAnimationResIdMethod.doesReturn(AnimationParams.Builder::class.java)
         }
 
-    /** Vendor API level 8 validation methods */
-    private fun isClassEmbeddingConfigurationBuilderApi8Valid(): Boolean =
-        validateReflection("Class EmbeddingConfiguration.Builder is not valid") {
-            val EmbeddingConfigurationBuilderClass = EmbeddingConfiguration.Builder::class.java
-            val setAutoSaveEmbeddingStateMethod =
-                EmbeddingConfigurationBuilderClass.getMethod(
-                    "setAutoSaveEmbeddingState",
-                    Boolean::class.java,
-                )
-            setAutoSaveEmbeddingStateMethod.isPublic &&
-                setAutoSaveEmbeddingStateMethod.doesReturn(
-                    EmbeddingConfiguration.Builder::class.java
-                )
-        }
-
     /** Overlay features validation methods */
     private fun isActivityStackGetTagValid(): Boolean =
         validateReflection("ActivityStack#getTag is not valid") {
diff --git a/xr/compose/compose-testing/api/restricted_current.txt b/xr/compose/compose-testing/api/restricted_current.txt
index 48ca38c..a72c5ac 100644
--- a/xr/compose/compose-testing/api/restricted_current.txt
+++ b/xr/compose/compose-testing/api/restricted_current.txt
@@ -216,7 +216,7 @@
     method @InaccessibleFromJava public androidx.xr.runtime.internal.ResizableComponent createResizableComponent(androidx.xr.runtime.internal.Dimensions minimumSize, androidx.xr.runtime.internal.Dimensions maximumSize);
     method @InaccessibleFromJava public androidx.xr.runtime.internal.SpatialPointerComponent createSpatialPointerComponent();
     method @InaccessibleFromJava @RestrictTo({androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX}) public androidx.xr.runtime.internal.SubspaceNodeEntity createSubspaceNodeEntity(androidx.xr.runtime.SubspaceNodeHolder<? extends java.lang.Object?> subspaceNodeHolder, androidx.xr.runtime.internal.Dimensions size);
-    method @InaccessibleFromJava public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(int stereoMode, androidx.xr.runtime.math.Pose pose, androidx.xr.runtime.internal.SurfaceEntity.CanvasShape canvasShape, @androidx.xr.runtime.internal.SurfaceEntity.ContentSecurityLevel int contentSecurityLevel, int superSampling, androidx.xr.runtime.internal.Entity parentEntity);
+    method @InaccessibleFromJava public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(int stereoMode, androidx.xr.runtime.math.Pose pose, androidx.xr.runtime.internal.SurfaceEntity.Shape shape, @androidx.xr.runtime.internal.SurfaceEntity.SurfaceProtection int surfaceProtection, int superSampling, androidx.xr.runtime.internal.Entity parentEntity);
     method @InaccessibleFromJava public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.MaterialResource> createWaterMaterial(boolean isAlphaMapVersion);
     method @InaccessibleFromJava public void destroyKhronosPbrMaterial(androidx.xr.runtime.internal.MaterialResource material);
     method @InaccessibleFromJava public void destroyTexture(androidx.xr.runtime.internal.TextureResource texture);
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialExternalSurface.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialExternalSurface.kt
index 55abeb2e..e2e6a9a 100644
--- a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialExternalSurface.kt
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialExternalSurface.kt
@@ -130,23 +130,25 @@
 public value class StereoMode private constructor(public val value: Int) {
     public companion object {
         /** Each eye will see the entire surface (no separation). */
-        public val Mono: StereoMode = StereoMode(SurfaceEntity.StereoMode.MONO)
+        public val Mono: StereoMode = StereoMode(SurfaceEntity.StereoMode.STEREO_MODE_MONO)
         /** The [top, bottom] halves of the surface will map to [left, right] eyes. */
-        public val TopBottom: StereoMode = StereoMode(SurfaceEntity.StereoMode.TOP_BOTTOM)
+        public val TopBottom: StereoMode =
+            StereoMode(SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM)
         /** The [left, right] halves of the surface will map to [left, right] eyes. */
-        public val SideBySide: StereoMode = StereoMode(SurfaceEntity.StereoMode.SIDE_BY_SIDE)
+        public val SideBySide: StereoMode =
+            StereoMode(SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE)
         /**
          * For displaying mv-hevc video format, [base, secondary] view layers will map to
          * [left, right] eyes.
          */
         public val MultiviewLeftPrimary: StereoMode =
-            StereoMode(SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY)
+            StereoMode(SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY)
         /**
          * For displaying mv-hevc video format, [base, secondary] view layers will map to
          * [right, left] eyes.
          */
         public val MultiviewRightPrimary: StereoMode =
-            StereoMode(SurfaceEntity.StereoMode.MULTIVIEW_RIGHT_PRIMARY)
+            StereoMode(SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY)
     }
 }
 
@@ -157,14 +159,14 @@
     public companion object {
         /** No security is applied. */
         public val None: SurfaceProtection =
-            SurfaceProtection(SurfaceEntity.ContentSecurityLevel.NONE)
+            SurfaceProtection(SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_NONE)
         /**
          * Sets the underlying Surface to set the
          * [android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT] flag. This is mainly used to
          * protect DRM video content.
          */
         public val Protected: SurfaceProtection =
-            SurfaceProtection(SurfaceEntity.ContentSecurityLevel.PROTECTED)
+            SurfaceProtection(SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_PROTECTED)
     }
 }
 
@@ -226,7 +228,7 @@
                 SurfaceEntity.create(
                     session = checkNotNull(session) { "Session is required" },
                     stereoMode = stereoMode.value,
-                    contentSecurityLevel = surfaceProtection.value,
+                    surfaceProtection = surfaceProtection.value,
                 ),
                 localDensity = density,
             )
@@ -391,12 +393,12 @@
                 SurfaceEntity.create(
                     session = checkNotNull(session) { "Session is required" },
                     stereoMode = stereoMode.value,
-                    contentSecurityLevel = surfaceProtection.value,
-                    canvasShape =
+                    surfaceProtection = surfaceProtection.value,
+                    shape =
                         if (isHemisphere) {
-                            SurfaceEntity.CanvasShape.Vr180Hemisphere(meterRadius)
+                            SurfaceEntity.Shape.Hemisphere(meterRadius)
                         } else {
-                            SurfaceEntity.CanvasShape.Vr360Sphere(meterRadius)
+                            SurfaceEntity.Shape.Sphere(meterRadius)
                         },
                 ),
                 headPose,
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialPanel.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialPanel.kt
index 1a1beb8..1a9d861 100644
--- a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialPanel.kt
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/SpatialPanel.kt
@@ -585,7 +585,7 @@
 
     SideEffect { corePanelEntity.setShape(shape, density) }
 
-    LaunchedEffect(intent) { corePanelEntity.launchActivity(intent) }
+    LaunchedEffect(intent) { corePanelEntity.startActivity(intent) }
 
     SpatialBox {
         SubspaceLayout(modifier = finalModifier, coreEntity = corePanelEntity) { _, constraints ->
diff --git a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/CoreEntity.kt b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/CoreEntity.kt
index 27446dc..9fee0b0 100644
--- a/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/CoreEntity.kt
+++ b/xr/compose/compose/src/main/kotlin/androidx/xr/compose/subspace/layout/CoreEntity.kt
@@ -30,6 +30,7 @@
 import androidx.xr.compose.unit.IntVolumeSize
 import androidx.xr.compose.unit.Meter
 import androidx.xr.runtime.Session
+import androidx.xr.runtime.math.FloatSize2d
 import androidx.xr.runtime.math.IntSize2d
 import androidx.xr.runtime.math.Pose
 import androidx.xr.runtime.math.Vector3
@@ -295,8 +296,8 @@
 
 internal class CoreActivityPanelEntity(private val activityPanelEntity: ActivityPanelEntity) :
     CoreBasePanelEntity(activityPanelEntity) {
-    fun launchActivity(intent: Intent, bundle: Bundle? = null) {
-        activityPanelEntity.launchActivity(intent, bundle)
+    fun startActivity(intent: Intent, bundle: Bundle? = null) {
+        activityPanelEntity.startActivity(intent, bundle)
     }
 }
 
@@ -351,10 +352,12 @@
         set(value) {
             if (super.size != value) {
                 super.size = value
-                surfaceEntity.canvasShape =
-                    SurfaceEntity.CanvasShape.Quad(
-                        Meter.fromPixel(size.width.toFloat(), localDensity).value,
-                        Meter.fromPixel(size.height.toFloat(), localDensity).value,
+                surfaceEntity.shape =
+                    SurfaceEntity.Shape.Quad(
+                        FloatSize2d(
+                            Meter.fromPixel(size.width.toFloat(), localDensity).value,
+                            Meter.fromPixel(size.height.toFloat(), localDensity).value,
+                        )
                     )
                 updateFeathering()
             }
@@ -377,8 +380,8 @@
 
     private fun updateFeathering() {
         (currentFeatheringEffect as? SpatialSmoothFeatheringEffect)?.let {
-            surfaceEntity.edgeFeather =
-                SurfaceEntity.EdgeFeatheringParams.SmoothFeather(
+            surfaceEntity.edgeFeatheringParams =
+                SurfaceEntity.EdgeFeatheringParams.RectangleFeather(
                     it.size.toWidthPercent(size.width.toFloat(), localDensity),
                     it.size.toHeightPercent(size.height.toFloat(), localDensity),
                 )
@@ -463,23 +466,23 @@
 
     /** Radius in meters. */
     internal var radius: Float
-        get() = radiusFromShape(surfaceEntity.canvasShape)
+        get() = radiusFromShape(surfaceEntity.shape)
         set(value) {
-            val shape = surfaceEntity.canvasShape
+            val shape = surfaceEntity.shape
             if (value != radiusFromShape(shape)) {
-                if (shape is SurfaceEntity.CanvasShape.Vr180Hemisphere) {
-                    surfaceEntity.canvasShape = SurfaceEntity.CanvasShape.Vr180Hemisphere(value)
+                if (shape is SurfaceEntity.Shape.Hemisphere) {
+                    surfaceEntity.shape = SurfaceEntity.Shape.Hemisphere(value)
                 } else {
-                    surfaceEntity.canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(value)
+                    surfaceEntity.shape = SurfaceEntity.Shape.Sphere(value)
                 }
                 updateFeathering()
             }
         }
 
-    private fun radiusFromShape(shape: SurfaceEntity.CanvasShape): Float {
-        if (shape is SurfaceEntity.CanvasShape.Vr180Hemisphere) {
+    private fun radiusFromShape(shape: SurfaceEntity.Shape): Float {
+        if (shape is SurfaceEntity.Shape.Hemisphere) {
             return shape.radius
-        } else if (shape is SurfaceEntity.CanvasShape.Vr360Sphere) {
+        } else if (shape is SurfaceEntity.Shape.Sphere) {
             return shape.radius
         }
         throw IllegalStateException("Shape must be spherical")
@@ -501,31 +504,28 @@
             return
         }
 
-        surfaceEntity.edgeFeather =
+        surfaceEntity.edgeFeatheringParams =
             if (!isBoundaryAvailable) {
                 val radius = if (isHemisphere) 0.5f else 0.7f
-                SurfaceEntity.EdgeFeatheringParams.SmoothFeather(radius, radius)
+                SurfaceEntity.EdgeFeatheringParams.RectangleFeather(radius, radius)
             } else {
                 val semicircleArcLength = Meter((radius * PI).toFloat()).toPx(localDensity)
                 (currentFeatheringEffect as? SpatialSmoothFeatheringEffect)?.let {
                     val radiusX =
                         it.size.toWidthPercent(
-                            if (
-                                surfaceEntity.canvasShape
-                                    is SurfaceEntity.CanvasShape.Vr180Hemisphere
-                            )
+                            if (surfaceEntity.shape is SurfaceEntity.Shape.Hemisphere)
                                 semicircleArcLength / 2
                             else semicircleArcLength,
                             localDensity,
                         )
                     val radiusY = it.size.toHeightPercent(semicircleArcLength, localDensity)
-                    SurfaceEntity.EdgeFeatheringParams.SmoothFeather(radiusX, radiusY)
+                    SurfaceEntity.EdgeFeatheringParams.RectangleFeather(radiusX, radiusY)
                 }
-            } ?: surfaceEntity.edgeFeather
+            } ?: surfaceEntity.edgeFeatheringParams
     }
 
     private val isHemisphere
-        get() = surfaceEntity.canvasShape is SurfaceEntity.CanvasShape.Vr180Hemisphere
+        get() = surfaceEntity.shape is SurfaceEntity.Shape.Hemisphere
 }
 
 /** [CoreEntity] types that implement this interface may have the ResizableComponent attached. */
diff --git a/xr/compose/integration-tests/layout/spatialcomposeapp/src/main/java/androidx/xr/compose/integration/layout/spatialcomposeapp/VideoPlayerActivity.kt b/xr/compose/integration-tests/layout/spatialcomposeapp/src/main/java/androidx/xr/compose/integration/layout/spatialcomposeapp/VideoPlayerActivity.kt
index 7d20f88..c934956 100644
--- a/xr/compose/integration-tests/layout/spatialcomposeapp/src/main/java/androidx/xr/compose/integration/layout/spatialcomposeapp/VideoPlayerActivity.kt
+++ b/xr/compose/integration-tests/layout/spatialcomposeapp/src/main/java/androidx/xr/compose/integration/layout/spatialcomposeapp/VideoPlayerActivity.kt
@@ -101,6 +101,7 @@
 import androidx.xr.compose.subspace.layout.width
 import androidx.xr.runtime.Session
 import androidx.xr.runtime.SessionCreateSuccess
+import androidx.xr.runtime.math.FloatSize2d
 import androidx.xr.runtime.math.FloatSize3d
 import androidx.xr.runtime.math.Pose
 import androidx.xr.runtime.math.Quaternion
@@ -688,15 +689,16 @@
                 surfaceEntity =
                     SurfaceEntity.create(
                         session = session,
-                        stereoMode = SurfaceEntity.StereoMode.MONO,
+                        stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO,
                         pose =
                             Pose(
                                 Vector3(0f, -0.45f, 0f),
                                 rotation = Quaternion(0.0f, 0.0f, 0.0f, 1.0f),
                             ),
-                        contentSecurityLevel =
-                            if (useDrmState.value) SurfaceEntity.ContentSecurityLevel.PROTECTED
-                            else SurfaceEntity.ContentSecurityLevel.NONE,
+                        surfaceProtection =
+                            if (useDrmState.value)
+                                SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_PROTECTED
+                            else SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_NONE,
                     )
                 // Make the video player movable (to make it easier to look at it from different
                 // angles and distances)
@@ -720,10 +722,9 @@
                             if (height > 0 && width > 0) {
                                 var dimensions =
                                     getCanvasAspectRatio(surfaceEntity!!.stereoMode, width, height)
-                                surfaceEntity!!.canvasShape =
-                                    SurfaceEntity.CanvasShape.Quad(
-                                        dimensions.width,
-                                        dimensions.height,
+                                surfaceEntity!!.shape =
+                                    SurfaceEntity.Shape.Quad(
+                                        FloatSize2d(dimensions.width, dimensions.height)
                                     )
 
                                 // Resize the MovableComponent to match the canvas dimensions.
@@ -865,39 +866,39 @@
                                 1.0f
                             }
 
-                        surfaceEntity!!.canvasShape =
-                            SurfaceEntity.CanvasShape.Quad(1.0f, canvasHeight)
+                        surfaceEntity!!.shape =
+                            SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, canvasHeight))
                     }
                 ) {
                     Text(text = "Set Quad", fontSize = 10.sp)
                 }
-                Button(
-                    onClick = {
-                        surfaceEntity!!.canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(5.0f)
-                    }
-                ) {
+                Button(onClick = { surfaceEntity!!.shape = SurfaceEntity.Shape.Sphere(1.0f) }) {
                     Text(text = "Set Vr360", fontSize = 10.sp)
                 }
-                Button(
-                    onClick = {
-                        surfaceEntity!!.canvasShape =
-                            SurfaceEntity.CanvasShape.Vr180Hemisphere(5.0f)
-                    }
-                ) {
+                Button(onClick = { surfaceEntity!!.shape = SurfaceEntity.Shape.Hemisphere(1.0f) }) {
                     Text(text = "Set Vr180", fontSize = 10.sp)
                 }
             } // end row
             Row(verticalAlignment = Alignment.CenterVertically) {
-                Button(onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.MONO }) {
+                Button(
+                    onClick = {
+                        surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO
+                    }
+                ) {
                     Text(text = "Mono", fontSize = 10.sp)
                 }
                 Button(
-                    onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM }
+                    onClick = {
+                        surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM
+                    }
                 ) {
                     Text(text = "Top-Bottom", fontSize = 10.sp)
                 }
                 Button(
-                    onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE }
+                    onClick = {
+                        surfaceEntity!!.stereoMode =
+                            SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE
+                    }
                 ) {
                     Text(text = "Side-by-Side", fontSize = 10.sp)
                 }
@@ -918,11 +919,11 @@
 
     fun getCanvasAspectRatio(stereoMode: Int, videoWidth: Int, videoHeight: Int): FloatSize3d {
         when (stereoMode) {
-            SurfaceEntity.StereoMode.MONO ->
+            SurfaceEntity.StereoMode.STEREO_MODE_MONO ->
                 return FloatSize3d(1.0f, videoHeight.toFloat() / videoWidth, 0.0f)
-            SurfaceEntity.StereoMode.TOP_BOTTOM ->
+            SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM ->
                 return FloatSize3d(1.0f, 0.5f * videoHeight.toFloat() / videoWidth, 0.0f)
-            SurfaceEntity.StereoMode.SIDE_BY_SIDE ->
+            SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE ->
                 return FloatSize3d(1.0f, 2.0f * videoHeight.toFloat() / videoWidth, 0.0f)
             else -> throw IllegalArgumentException("Unsupported stereo mode: $stereoMode")
         }
diff --git a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/accessibility/AccessibilityActivity.kt b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/accessibility/AccessibilityActivity.kt
index 1d78d2c..d35320f 100644
--- a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/accessibility/AccessibilityActivity.kt
+++ b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/accessibility/AccessibilityActivity.kt
@@ -64,6 +64,7 @@
 import androidx.xr.compose.unit.DpVolumeSize
 import androidx.xr.runtime.Session
 import androidx.xr.runtime.SessionCreateSuccess
+import androidx.xr.runtime.math.FloatSize2d
 import androidx.xr.runtime.math.Pose
 import androidx.xr.runtime.math.Vector3
 import androidx.xr.scenecore.AnchorEntity
@@ -241,27 +242,25 @@
         var surfaceEntity by remember { mutableStateOf<SurfaceEntity?>(null) }
         val radius = 0.65f
         var shape by remember {
-            mutableStateOf<SurfaceEntity.CanvasShape>(
-                SurfaceEntity.CanvasShape.Vr180Hemisphere(radius)
-            )
+            mutableStateOf<SurfaceEntity.Shape>(SurfaceEntity.Shape.Hemisphere(radius))
         }
 
         Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
             Column {
                 LabeledRadioButton(
                     "Quad",
-                    shape is SurfaceEntity.CanvasShape.Quad,
-                    { shape = SurfaceEntity.CanvasShape.Quad(radius * 2f, radius * 2f) },
+                    shape is SurfaceEntity.Shape.Quad,
+                    { shape = SurfaceEntity.Shape.Quad(FloatSize2d(radius * 2f, radius * 2f)) },
                 )
                 LabeledRadioButton(
-                    "Vr180Hemisphere",
-                    shape is SurfaceEntity.CanvasShape.Vr180Hemisphere,
-                    { shape = SurfaceEntity.CanvasShape.Vr180Hemisphere(radius) },
+                    "Hemisphere",
+                    shape is SurfaceEntity.Shape.Hemisphere,
+                    { shape = SurfaceEntity.Shape.Hemisphere(radius) },
                 )
                 LabeledRadioButton(
-                    "Vr360Sphere",
-                    shape is SurfaceEntity.CanvasShape.Vr360Sphere,
-                    { shape = SurfaceEntity.CanvasShape.Vr360Sphere(radius) },
+                    "Sphere",
+                    shape is SurfaceEntity.Shape.Sphere,
+                    { shape = SurfaceEntity.Shape.Sphere(radius) },
                 )
             }
             Button({
@@ -270,7 +269,7 @@
                         SurfaceEntity.create(
                             session,
                             pose = Pose(Vector3(-1f, 0f, -0.5f)),
-                            canvasShape = shape,
+                            shape = shape,
                         )
                     surfaceEntity?.contentDescription = "${shape.javaClass.simpleName} Surface"
                 }
diff --git a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/spatialcompose/SpatialComposeVideoPlayer.kt b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/spatialcompose/SpatialComposeVideoPlayer.kt
index bb1471b..865c888 100644
--- a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/spatialcompose/SpatialComposeVideoPlayer.kt
+++ b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/spatialcompose/SpatialComposeVideoPlayer.kt
@@ -105,6 +105,7 @@
 import androidx.xr.runtime.Session
 import androidx.xr.runtime.SessionCreateSuccess
 import androidx.xr.runtime.internal.Dimensions
+import androidx.xr.runtime.math.FloatSize2d
 import androidx.xr.runtime.math.FloatSize3d
 import androidx.xr.runtime.math.Pose
 import androidx.xr.runtime.math.Quaternion
@@ -794,9 +795,9 @@
             onClick = {
                 surfaceEntity =
                     SurfaceEntity.create(
-                        session,
-                        SurfaceEntity.StereoMode.TOP_BOTTOM,
-                        Pose(Vector3(0f, -0.45f, 0f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
+                        session = session,
+                        pose = Pose(Vector3(0f, -0.45f, 0f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
+                        stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM,
                     )
                 // Make the video player movable (to make it easier to look at it from different
                 // angles and distances)
@@ -820,8 +821,8 @@
                     // Resize the canvas to match the video aspect ratio - accounting for the stereo
                     // mode.
                     var dimensions = getCanvasAspectRatio(surfaceEntity!!.stereoMode, width, height)
-                    surfaceEntity!!.canvasShape =
-                        SurfaceEntity.CanvasShape.Quad(dimensions.width, dimensions.height)
+                    surfaceEntity!!.shape =
+                        SurfaceEntity.Shape.Quad(FloatSize2d(dimensions.width, dimensions.height))
 
                     // Resize the MovableComponent to match the canvas dimensions.
                     movableComponent!!.size = surfaceEntity!!.dimensions
@@ -949,39 +950,39 @@
                                 1.0f
                             }
 
-                        surfaceEntity!!.canvasShape =
-                            SurfaceEntity.CanvasShape.Quad(1.0f, canvasHeight)
+                        surfaceEntity!!.shape =
+                            SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, canvasHeight))
                     }
                 ) {
                     Text(text = "Set Quad", fontSize = 10.sp)
                 }
-                Button(
-                    onClick = {
-                        surfaceEntity!!.canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(5.0f)
-                    }
-                ) {
+                Button(onClick = { surfaceEntity!!.shape = SurfaceEntity.Shape.Sphere(5.0f) }) {
                     Text(text = "Set Vr360", fontSize = 10.sp)
                 }
-                Button(
-                    onClick = {
-                        surfaceEntity!!.canvasShape =
-                            SurfaceEntity.CanvasShape.Vr180Hemisphere(5.0f)
-                    }
-                ) {
+                Button(onClick = { surfaceEntity!!.shape = SurfaceEntity.Shape.Hemisphere(5.0f) }) {
                     Text(text = "Set Vr180", fontSize = 10.sp)
                 }
             }
             Row(verticalAlignment = Alignment.CenterVertically) {
-                Button(onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.MONO }) {
+                Button(
+                    onClick = {
+                        surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO
+                    }
+                ) {
                     Text(text = "Mono", fontSize = 10.sp)
                 }
                 Button(
-                    onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM }
+                    onClick = {
+                        surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM
+                    }
                 ) {
                     Text(text = "Top-Bottom", fontSize = 10.sp)
                 }
                 Button(
-                    onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE }
+                    onClick = {
+                        surfaceEntity!!.stereoMode =
+                            SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE
+                    }
                 ) {
                     Text(text = "Side-by-Side", fontSize = 10.sp)
                 }
@@ -1002,11 +1003,11 @@
 
     fun getCanvasAspectRatio(stereoMode: Int, videoWidth: Int, videoHeight: Int): Dimensions {
         when (stereoMode) {
-            SurfaceEntity.StereoMode.MONO ->
+            SurfaceEntity.StereoMode.STEREO_MODE_MONO ->
                 return Dimensions(1.0f, videoHeight.toFloat() / videoWidth, 0.0f)
-            SurfaceEntity.StereoMode.TOP_BOTTOM ->
+            SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM ->
                 return Dimensions(1.0f, 0.5f * videoHeight.toFloat() / videoWidth, 0.0f)
-            SurfaceEntity.StereoMode.SIDE_BY_SIDE ->
+            SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE ->
                 return Dimensions(1.0f, 2.0f * videoHeight.toFloat() / videoWidth, 0.0f)
             else -> throw IllegalArgumentException("Unsupported stereo mode: $stereoMode")
         }
diff --git a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/videoplayer/VideoPlayerActivity.kt b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/videoplayer/VideoPlayerActivity.kt
index e27847a..22f7b07 100644
--- a/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/videoplayer/VideoPlayerActivity.kt
+++ b/xr/compose/integration-tests/testapp/src/main/kotlin/androidx/xr/compose/testapp/videoplayer/VideoPlayerActivity.kt
@@ -85,6 +85,7 @@
 import androidx.xr.runtime.Session
 import androidx.xr.runtime.SessionCreateSuccess
 import androidx.xr.runtime.internal.Dimensions
+import androidx.xr.runtime.math.FloatSize2d
 import androidx.xr.runtime.math.FloatSize3d
 import androidx.xr.runtime.math.IntSize2d
 import androidx.xr.runtime.math.Pose
@@ -238,20 +239,20 @@
         val effectiveDisplayWidth = videoWidth.toFloat() * pixelAspectRatio
 
         return when (stereoMode) {
-            SurfaceEntity.StereoMode.MONO,
-            SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
-            SurfaceEntity.StereoMode.MULTIVIEW_RIGHT_PRIMARY ->
+            SurfaceEntity.StereoMode.STEREO_MODE_MONO,
+            SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
+            SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY ->
                 FloatSize3d(1.0f, videoHeight.toFloat() / effectiveDisplayWidth, 0.0f)
-            SurfaceEntity.StereoMode.TOP_BOTTOM ->
+            SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM ->
                 FloatSize3d(1.0f, 0.5f * videoHeight.toFloat() / effectiveDisplayWidth, 0.0f)
-            SurfaceEntity.StereoMode.SIDE_BY_SIDE ->
+            SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE ->
                 FloatSize3d(1.0f, 2.0f * videoHeight.toFloat() / effectiveDisplayWidth, 0.0f)
             else -> throw IllegalArgumentException("Unsupported stereo mode: $stereoMode")
         }
     }
 
     private fun quad() {
-        surfaceEntity!!.canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f)
+        surfaceEntity!!.shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f))
         // Move the Quad-shaped canvas to a spot in front of the User.
         surfaceEntity!!.setPose(
             session.scene.spatialUser.head?.transformPoseTo(
@@ -476,8 +477,8 @@
                 value = featherRadiusX,
                 onValueChange = {
                     featherRadiusX = it
-                    surfaceEntity!!.edgeFeather =
-                        SurfaceEntity.EdgeFeatheringParams.SmoothFeather(
+                    surfaceEntity!!.edgeFeatheringParams =
+                        SurfaceEntity.EdgeFeatheringParams.RectangleFeather(
                             featherRadiusX,
                             featherRadiusY,
                         )
@@ -489,8 +490,8 @@
                 value = featherRadiusY,
                 onValueChange = {
                     featherRadiusY = it
-                    surfaceEntity!!.edgeFeather =
-                        SurfaceEntity.EdgeFeatheringParams.SmoothFeather(
+                    surfaceEntity!!.edgeFeatheringParams =
+                        SurfaceEntity.EdgeFeatheringParams.RectangleFeather(
                             featherRadiusX,
                             featherRadiusY,
                         )
@@ -554,12 +555,11 @@
                     ApiButton("Set Quad", modifier) { quad() }
 
                     ApiButton("Set Vr360", modifier) {
-                        surfaceEntity!!.canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(1.0f)
+                        surfaceEntity!!.shape = SurfaceEntity.Shape.Sphere(1.0f)
                     }
 
                     ApiButton("Set Vr180", modifier) {
-                        surfaceEntity!!.canvasShape =
-                            SurfaceEntity.CanvasShape.Vr180Hemisphere(1.0f)
+                        surfaceEntity!!.shape = SurfaceEntity.Shape.Hemisphere(1.0f)
                     }
                 }
 
@@ -568,15 +568,16 @@
                 ApiRow {
                     val modifier = Modifier.weight(1F)
                     ApiButton("Mono", modifier) {
-                        surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.MONO
+                        surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO
                     }
 
                     ApiButton("Top-Bottom", modifier) {
-                        surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM
+                        surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM
                     }
 
                     ApiButton("Side-by-Side", modifier) {
-                        surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE
+                        surfaceEntity!!.stereoMode =
+                            SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE
                     }
                 }
             }
@@ -637,20 +638,26 @@
     private fun createSurfaceEntity(
         stereoMode: Int,
         pose: Pose,
-        canvasShape: SurfaceEntity.CanvasShape,
+        canvasShape: SurfaceEntity.Shape,
         protected: Boolean = false,
     ) {
         // Create SurfaceEntity and MovableComponent if they don't exist.
         if (surfaceEntity == null) {
             val surfaceContentLevel =
                 if (protected) {
-                    SurfaceEntity.ContentSecurityLevel.PROTECTED
+                    SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_PROTECTED
                 } else {
-                    SurfaceEntity.ContentSecurityLevel.NONE
+                    SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_NONE
                 }
 
             surfaceEntity =
-                SurfaceEntity.create(session, stereoMode, pose, canvasShape, surfaceContentLevel)
+                SurfaceEntity.create(
+                    session = session,
+                    pose = pose,
+                    shape = canvasShape,
+                    stereoMode = stereoMode,
+                    surfaceProtection = surfaceContentLevel,
+                )
             // Make the video player movable (to make it easier to look at it from different
             // angles and distances)
             movableComponent = MovableComponent.createSystemMovable(session)
@@ -695,7 +702,7 @@
     private fun setupExoPlayer(
         videoUri: String,
         stereoMode: Int,
-        canvasShape: SurfaceEntity.CanvasShape,
+        canvasShape: SurfaceEntity.Shape,
         protected: Boolean,
     ) {
         val drmLicenseUrl = "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
@@ -737,9 +744,11 @@
                         getCanvasAspectRatio(stereoMode, width, height, currentPixelAspectRatio)
                     // Set the dimensions of the Quad canvas to the video dimensions and attach the
                     // a MovableComponent.
-                    if (canvasShape is SurfaceEntity.CanvasShape.Quad) {
-                        surfaceEntity?.canvasShape =
-                            SurfaceEntity.CanvasShape.Quad(dimensions.width, dimensions.height)
+                    if (canvasShape is SurfaceEntity.Shape.Quad) {
+                        surfaceEntity?.shape =
+                            SurfaceEntity.Shape.Quad(
+                                FloatSize2d(dimensions.width, dimensions.height)
+                            )
                         movableComponent?.size =
                             (surfaceEntity?.dimensions ?: Dimensions(1.0f, 1.0f, 1.0f))
                                 as FloatSize3d
@@ -803,7 +812,7 @@
 
     companion object {
         val defaultPose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f))
-        val defaultCanvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f)
+        val defaultShape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f))
         var videoAttributesMap: IntObjectMap<VideoAttributes> =
             MutableIntObjectMap<VideoAttributes>(9).apply {
                 put(
@@ -811,10 +820,10 @@
                     VideoAttributes(
                         "Play Big Buck Bunny",
                         "/Download/vid_bigbuckbunny.mp4",
-                        SurfaceEntity.StereoMode.TOP_BOTTOM,
+                        SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM,
                         false,
                         defaultPose,
-                        defaultCanvasShape,
+                        defaultShape,
                     ),
                 )
                 put(
@@ -822,10 +831,10 @@
                     VideoAttributes(
                         "Play MVHEVC Left Primary",
                         "/Download/mvhevc_flat_left_primary_1080.mov",
-                        SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+                        SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
                         false,
                         defaultPose,
-                        defaultCanvasShape,
+                        defaultShape,
                     ),
                 )
                 put(
@@ -833,10 +842,10 @@
                     VideoAttributes(
                         "Play MVHEVC Right Primary",
                         "/Download/mvhevc_flat_right_primary_1080.mov",
-                        SurfaceEntity.StereoMode.MULTIVIEW_RIGHT_PRIMARY,
+                        SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY,
                         false,
                         defaultPose,
-                        defaultCanvasShape,
+                        defaultShape,
                     ),
                 )
                 put(
@@ -844,10 +853,10 @@
                     VideoAttributes(
                         "Play Naver 180 (Side-by-Side)",
                         "/Download/Naver180.mp4",
-                        SurfaceEntity.StereoMode.SIDE_BY_SIDE,
+                        SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE,
                         false,
                         defaultPose,
-                        SurfaceEntity.CanvasShape.Vr180Hemisphere(1.0f),
+                        SurfaceEntity.Shape.Hemisphere(1.0f),
                     ),
                 )
                 put(
@@ -855,10 +864,10 @@
                     VideoAttributes(
                         "Play Naver 180 (MV-HEVC)",
                         "/Download/Naver180_MV-HEVC.mp4",
-                        SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+                        SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
                         false,
                         defaultPose,
-                        SurfaceEntity.CanvasShape.Vr180Hemisphere(1.0f),
+                        SurfaceEntity.Shape.Hemisphere(1.0f),
                     ),
                 )
                 put(
@@ -866,10 +875,10 @@
                     VideoAttributes(
                         "Play Galaxy 360 (Top-Bottom)",
                         "/Download/Galaxy11_VR_3D360.mp4",
-                        SurfaceEntity.StereoMode.TOP_BOTTOM,
+                        SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM,
                         false,
                         defaultPose,
-                        SurfaceEntity.CanvasShape.Vr360Sphere(1.0f),
+                        SurfaceEntity.Shape.Sphere(1.0f),
                     ),
                 )
                 put(
@@ -877,10 +886,10 @@
                     VideoAttributes(
                         "Play Galaxy 360 (MV-HEVC)",
                         "/Download/Galaxy11_VR_3D360_MV-HEVC.mp4",
-                        SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+                        SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
                         false,
                         defaultPose,
-                        SurfaceEntity.CanvasShape.Vr360Sphere(1.0f),
+                        SurfaceEntity.Shape.Sphere(1.0f),
                     ),
                 )
                 put(
@@ -888,10 +897,10 @@
                     VideoAttributes(
                         "Play DRM Protected For Bigger Blazes",
                         "/Download/sdr_singleview_protected.mp4",
-                        SurfaceEntity.StereoMode.SIDE_BY_SIDE,
+                        SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE,
                         true,
                         defaultPose,
-                        defaultCanvasShape,
+                        defaultShape,
                     ),
                 )
                 put(
@@ -899,10 +908,10 @@
                     VideoAttributes(
                         "Play DRM Protected MVHEVC Left Primary",
                         "/Download/mvhevc_flat_left_primary_1080_protected.mp4",
-                        SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+                        SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
                         true,
                         defaultPose,
-                        defaultCanvasShape,
+                        defaultShape,
                     ),
                 )
             }
@@ -926,6 +935,6 @@
         val stereoMode: Int,
         val protected: Boolean,
         var pose: Pose,
-        var canvasShape: SurfaceEntity.CanvasShape,
+        var canvasShape: SurfaceEntity.Shape,
     )
 }
diff --git a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/NavigationRail.kt b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/NavigationRail.kt
index 7650940..8d9a7c41 100644
--- a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/NavigationRail.kt
+++ b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/NavigationRail.kt
@@ -22,6 +22,8 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.widthIn
 import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.CornerSize
 import androidx.compose.material3.ExperimentalMaterial3ComponentOverrideApi
 import androidx.compose.material3.FloatingActionButton
 import androidx.compose.material3.LocalContentColor
@@ -44,6 +46,7 @@
 import androidx.xr.compose.spatial.ContentEdge
 import androidx.xr.compose.spatial.Orbiter
 import androidx.xr.compose.spatial.OrbiterOffsetType
+import androidx.xr.compose.subspace.layout.SpatialRoundedCornerShape
 
 /**
  * <a href="https://m3.material.io/components/navigation-rail/overview" class="external"
@@ -84,30 +87,34 @@
     header: @Composable (ColumnScope.() -> Unit)? = null,
     content: @Composable ColumnScope.() -> Unit,
 ) {
-    val orbiterProperties = LocalNavigationRailOrbiterProperties.current
+    val orbiterProperties =
+        LocalNavigationRailOrbiterProperties.current.copy(
+            shape = SpatialRoundedCornerShape(CornerSize(percent = 0))
+        )
     VerticalOrbiter(orbiterProperties) {
-        Surface(color = containerColor, contentColor = contentColor, modifier = modifier) {
-            Column(
-                // XR-changed: Original NavigationRail uses fillMaxHeight() and windowInsets,
-                // which do not produce the desired result in XR.
-                Modifier.widthIn(min = XrNavigationRailTokens.ContainerWidth)
-                    .padding(vertical = XrNavigationRailTokens.VerticalPadding)
-                    .selectableGroup(),
-                horizontalAlignment = Alignment.CenterHorizontally,
-                verticalArrangement = Arrangement.spacedBy(XrNavigationRailTokens.VerticalPadding),
-                content = content,
-            )
-        }
-    }
-    // Header goes inside a separate top-aligned Orbiter without an outline shape, as this is
-    // generally a FAB.
-    if (header != null) {
-        VerticalOrbiter(orbiterProperties.copy(alignment = Alignment.Top)) {
-            Column(
-                horizontalAlignment = Alignment.CenterHorizontally,
-                verticalArrangement = Arrangement.spacedBy(XrNavigationRailTokens.VerticalPadding),
-                content = header,
-            )
+        Column(
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalArrangement = Arrangement.spacedBy(XrNavigationRailTokens.VerticalPadding),
+        ) {
+            header?.let { it() }
+            Surface(
+                shape = CircleShape,
+                color = containerColor,
+                contentColor = contentColor,
+                modifier = modifier,
+            ) {
+                Column(
+                    // XR-changed: Original NavigationRail uses fillMaxHeight() and windowInsets,
+                    // which do not produce the desired result in XR.
+                    Modifier.widthIn(min = XrNavigationRailTokens.ContainerWidth)
+                        .padding(vertical = XrNavigationRailTokens.VerticalPadding)
+                        .selectableGroup(),
+                    horizontalAlignment = Alignment.CenterHorizontally,
+                    verticalArrangement =
+                        Arrangement.spacedBy(XrNavigationRailTokens.VerticalPadding),
+                    content = content,
+                )
+            }
         }
     }
 }
diff --git a/xr/runtime/runtime-testing/api/restricted_current.txt b/xr/runtime/runtime-testing/api/restricted_current.txt
index 4f8d3cb..f682fb9 100644
--- a/xr/runtime/runtime-testing/api/restricted_current.txt
+++ b/xr/runtime/runtime-testing/api/restricted_current.txt
@@ -190,7 +190,7 @@
     method public androidx.xr.runtime.testing.FakeResizableComponent createResizableComponent(androidx.xr.runtime.internal.Dimensions minimumSize, androidx.xr.runtime.internal.Dimensions maximumSize);
     method public androidx.xr.runtime.internal.SpatialPointerComponent createSpatialPointerComponent();
     method public androidx.xr.runtime.internal.SubspaceNodeEntity createSubspaceNodeEntity(androidx.xr.runtime.SubspaceNodeHolder<? extends java.lang.Object?> subspaceNodeHolder, androidx.xr.runtime.internal.Dimensions size);
-    method public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(int stereoMode, androidx.xr.runtime.math.Pose pose, androidx.xr.runtime.internal.SurfaceEntity.CanvasShape canvasShape, int contentSecurityLevel, int superSampling, androidx.xr.runtime.internal.Entity parentEntity);
+    method public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(int stereoMode, androidx.xr.runtime.math.Pose pose, androidx.xr.runtime.internal.SurfaceEntity.Shape shape, int surfaceProtection, int superSampling, androidx.xr.runtime.internal.Entity parentEntity);
     method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.MaterialResource> createWaterMaterial(boolean isAlphaMapVersion);
     method public void destroyKhronosPbrMaterial(androidx.xr.runtime.internal.MaterialResource material);
     method public void destroyTexture(androidx.xr.runtime.internal.TextureResource texture);
@@ -584,6 +584,11 @@
     method public void dispose();
     method public java.util.List<androidx.xr.runtime.testing.FakeRenderingRuntime.FakeKhronosPbrMaterial> getCreatedKhronosPbrMaterials();
     method public java.util.List<androidx.xr.runtime.testing.FakeRenderingRuntime.FakeWaterMaterial> getCreatedWaterMaterials();
+    method public androidx.xr.runtime.internal.TextureResource? getReflectionTextureFromIbl(androidx.xr.runtime.internal.ExrImageResource iblToken);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.ExrImageResource> loadExrImageByAssetName(String assetName);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.ExrImageResource> loadExrImageByByteArray(byte[] assetData, String assetKey);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.GltfModelResource> loadGltfByAssetName(String assetName);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.GltfModelResource> loadGltfByByteArray(byte[] assetData, String assetKey);
     method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.TextureResource> loadTexture(String assetName, androidx.xr.runtime.internal.TextureSampler sampler);
     method public void setAlphaCutoffOnKhronosPbrMaterial(androidx.xr.runtime.internal.MaterialResource material, float alphaCutoff);
     method public void setAlphaMapOnWaterMaterial(androidx.xr.runtime.internal.MaterialResource material, androidx.xr.runtime.internal.TextureResource alphaMap);
@@ -1103,7 +1108,6 @@
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class FakeSurfaceEntity extends androidx.xr.runtime.testing.FakeEntity implements androidx.xr.runtime.internal.SurfaceEntity {
     ctor public FakeSurfaceEntity();
     method public androidx.xr.runtime.internal.TextureResource? getAuxiliaryAlphaMask();
-    method public androidx.xr.runtime.internal.SurfaceEntity.CanvasShape getCanvasShape();
     method public int getColorRange();
     method public int getColorSpace();
     method public int getColorTransfer();
@@ -1111,24 +1115,24 @@
     method public androidx.xr.runtime.internal.Dimensions getDimensions();
     method public androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather getEdgeFeather();
     method public boolean getMContentColorMetadataSet();
-    method public int getMaxCLL();
+    method public int getMaxContentLightLevel();
     method public androidx.xr.runtime.internal.PerceivedResolutionResult getPerceivedResolution();
     method public androidx.xr.runtime.internal.PerceivedResolutionResult getPerceivedResolutionResult();
     method public androidx.xr.runtime.internal.TextureResource? getPrimaryAlphaMask();
+    method public androidx.xr.runtime.internal.SurfaceEntity.Shape getShape();
     method public int getStereoMode();
     method public android.view.Surface getSurface();
     method public void resetContentColorMetadata();
     method public void setAuxiliaryAlphaMaskTexture(androidx.xr.runtime.internal.TextureResource? alphaMask);
-    method public void setCanvasShape(androidx.xr.runtime.internal.SurfaceEntity.CanvasShape);
-    method public void setContentColorMetadata(int colorSpace, int colorTransfer, int colorRange, int maxCLL);
+    method public void setContentColorMetadata(int colorSpace, int colorTransfer, int colorRange, int maxContentLightLevel);
     method public void setEdgeFeather(androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather);
     method public void setMContentColorMetadataSet(boolean);
     method public void setPerceivedResolutionResult(androidx.xr.runtime.internal.PerceivedResolutionResult);
     method public void setPrimaryAlphaMaskTexture(androidx.xr.runtime.internal.TextureResource? alphaMask);
+    method public void setShape(androidx.xr.runtime.internal.SurfaceEntity.Shape);
     method public void setStereoMode(int);
     method public void setSurface(android.view.Surface surface);
     property public androidx.xr.runtime.internal.TextureResource? auxiliaryAlphaMask;
-    property public androidx.xr.runtime.internal.SurfaceEntity.CanvasShape canvasShape;
     property public int colorRange;
     property public int colorSpace;
     property public int colorTransfer;
@@ -1136,9 +1140,10 @@
     property public androidx.xr.runtime.internal.Dimensions dimensions;
     property public androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather edgeFeather;
     property public boolean mContentColorMetadataSet;
-    property public int maxCLL;
+    property public int maxContentLightLevel;
     property public androidx.xr.runtime.internal.PerceivedResolutionResult perceivedResolutionResult;
     property public androidx.xr.runtime.internal.TextureResource? primaryAlphaMask;
+    property public androidx.xr.runtime.internal.SurfaceEntity.Shape shape;
     property public int stereoMode;
     property public android.view.Surface surface;
   }
diff --git a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapter.kt b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapter.kt
index d229aa3..e743016 100644
--- a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapter.kt
+++ b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapter.kt
@@ -549,15 +549,15 @@
     override fun createSurfaceEntity(
         stereoMode: Int,
         pose: Pose,
-        canvasShape: SurfaceEntity.CanvasShape,
-        contentSecurityLevel: Int,
+        shape: SurfaceEntity.Shape,
+        surfaceProtection: Int,
         superSampling: Int,
         parentEntity: Entity,
     ): SurfaceEntity {
         val surfaceEntity = FakeSurfaceEntity()
         surfaceEntity.stereoMode = stereoMode
         surfaceEntity.setPose(pose)
-        surfaceEntity.canvasShape = canvasShape
+        surfaceEntity.shape = shape
         surfaceEntity.parent = parentEntity
 
         return surfaceEntity
diff --git a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRenderingRuntime.kt b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRenderingRuntime.kt
index 8375286..2da5d02 100644
--- a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRenderingRuntime.kt
+++ b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeRenderingRuntime.kt
@@ -18,6 +18,8 @@
 
 import android.app.Activity
 import androidx.annotation.RestrictTo
+import androidx.xr.runtime.internal.ExrImageResource
+import androidx.xr.runtime.internal.GltfModelResource
 import androidx.xr.runtime.internal.KhronosPbrMaterialSpec
 import androidx.xr.runtime.internal.MaterialResource
 import androidx.xr.runtime.internal.RenderingRuntime
@@ -38,6 +40,26 @@
     private val activity: Activity,
 ) : RenderingRuntime {
     @Suppress("AsyncSuffixFuture")
+    override fun loadGltfByAssetName(assetName: String): ListenableFuture<GltfModelResource> =
+        immediateFuture(FakeGltfModelResource(0))
+
+    @Suppress("AsyncSuffixFuture")
+    override fun loadGltfByByteArray(
+        assetData: ByteArray,
+        assetKey: String,
+    ): ListenableFuture<GltfModelResource> = immediateFuture(FakeGltfModelResource(0))
+
+    @Suppress("AsyncSuffixFuture")
+    override fun loadExrImageByAssetName(assetName: String): ListenableFuture<ExrImageResource> =
+        immediateFuture(FakeExrImageResource(0))
+
+    @Suppress("AsyncSuffixFuture")
+    override fun loadExrImageByByteArray(
+        assetData: ByteArray,
+        assetKey: String,
+    ): ListenableFuture<ExrImageResource> = immediateFuture(FakeExrImageResource(1))
+
+    @Suppress("AsyncSuffixFuture")
     override fun loadTexture(
         assetName: String,
         sampler: TextureSampler,
@@ -64,6 +86,10 @@
         reflectionTexture = null
     }
 
+    override fun getReflectionTextureFromIbl(iblToken: ExrImageResource): TextureResource? {
+        return reflectionTexture
+    }
+
     /**
      * For test purposes only.
      *
diff --git a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntity.kt b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntity.kt
index 07c7b95..4644f5d 100644
--- a/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntity.kt
+++ b/xr/runtime/runtime-testing/src/main/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntity.kt
@@ -23,8 +23,9 @@
 import androidx.xr.runtime.internal.Dimensions
 import androidx.xr.runtime.internal.PerceivedResolutionResult
 import androidx.xr.runtime.internal.SurfaceEntity
-import androidx.xr.runtime.internal.SurfaceEntity.CanvasShape
+import androidx.xr.runtime.internal.SurfaceEntity.Shape
 import androidx.xr.runtime.internal.TextureResource
+import androidx.xr.runtime.math.FloatSize2d
 
 /**
  * Test-only implementation of [SurfaceEntity].
@@ -45,7 +46,7 @@
     override var stereoMode: Int = SurfaceEntity.StereoMode.SIDE_BY_SIDE
 
     /** Specifies the shape of the spatial canvas which the surface is texture mapped to. */
-    override var canvasShape: CanvasShape = CanvasShape.Quad(0f, 0f)
+    override var shape: Shape = Shape.Quad(FloatSize2d(0f, 0f))
 
     /**
      * Retrieves the dimensions of the "spatial canvas" which the surface is mapped to. These values
@@ -54,7 +55,7 @@
      * @return The canvas [Dimensions].
      */
     override val dimensions: Dimensions
-        get() = canvasShape.dimensions
+        get() = shape.dimensions
 
     private var _surface: Surface =
         ImageReader.newInstance(1, 1, ImageFormat.YUV_420_888, 1).surface
@@ -184,14 +185,14 @@
     override val colorRange: Int
         get() = _colorRange
 
-    private var _maxCLL: Int = 0
+    private var _maxContentLightLevel: Int = 0
 
     /**
      * The active maximum content light level (MaxCLL) in nits. A value of 0 indicates that MaxCLL
      * is not set or is unknown. This value is used if [contentColorMetadataSet] is `true`.
      */
-    override val maxCLL: Int
-        get() = _maxCLL
+    override val maxContentLightLevel: Int
+        get() = _maxContentLightLevel
 
     /**
      * Sets the explicit color information for the surface content. This will also set
@@ -207,12 +208,12 @@
         colorSpace: Int,
         colorTransfer: Int,
         colorRange: Int,
-        maxCLL: Int,
+        maxContentLightLevel: Int,
     ) {
         _colorSpace = colorSpace
         _colorTransfer = colorTransfer
         _colorRange = colorRange
-        _maxCLL = maxCLL
+        _maxContentLightLevel = maxContentLightLevel
     }
 
     /**
@@ -224,7 +225,7 @@
         _colorSpace = SurfaceEntity.ColorSpace.BT709
         _colorTransfer = SurfaceEntity.ColorTransfer.LINEAR
         _colorRange = SurfaceEntity.ColorRange.FULL
-        _maxCLL = 0
+        _maxContentLightLevel = 0
     }
 
     /**
@@ -232,5 +233,5 @@
      *
      * @throws IllegalStateException if the Entity has been disposed.
      */
-    override var edgeFeather: SurfaceEntity.EdgeFeather = SurfaceEntity.EdgeFeather.SolidEdge()
+    override var edgeFeather: SurfaceEntity.EdgeFeather = SurfaceEntity.EdgeFeather.NoFeathering()
 }
diff --git a/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapterTest.kt b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapterTest.kt
index 1ec3d63..f8fd565 100644
--- a/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapterTest.kt
+++ b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeJxrPlatformAdapterTest.kt
@@ -281,7 +281,7 @@
     fun createSurfaceEntity_returnsInitialValue() {
         val stereoMode = 0
         val pose = Pose.Identity
-        val canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(1.0f)
+        val canvasShape = SurfaceEntity.Shape.Sphere(1.0f)
         val contentSecurityLevel = 0
         val superSampling = 0
         val parentEntity = FakeEntity()
@@ -298,7 +298,7 @@
         assertThat(surfaceEntity).isInstanceOf(FakeSurfaceEntity::class.java)
         assertThat(surfaceEntity.stereoMode).isEqualTo(stereoMode)
         assertThat(surfaceEntity.getPose()).isEqualTo(pose)
-        assertThat(surfaceEntity.canvasShape).isEqualTo(canvasShape)
+        assertThat(surfaceEntity.shape).isEqualTo(canvasShape)
         assertThat(surfaceEntity.parent).isEqualTo(parentEntity)
     }
 
diff --git a/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntityTest.kt b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntityTest.kt
index 7208b67..9e815fd 100644
--- a/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntityTest.kt
+++ b/xr/runtime/runtime-testing/src/test/kotlin/androidx/xr/runtime/testing/FakeSurfaceEntityTest.kt
@@ -22,6 +22,7 @@
 import androidx.xr.runtime.internal.PixelDimensions
 import androidx.xr.runtime.internal.SurfaceEntity
 import androidx.xr.runtime.internal.TextureResource
+import androidx.xr.runtime.math.FloatSize2d
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
@@ -31,7 +32,7 @@
 
 @RunWith(RobolectricTestRunner::class)
 class FakeSurfaceEntityTest {
-    val testCanvasShape = SurfaceEntity.CanvasShape.Quad(1f, 1f)
+    val testCanvasShape = SurfaceEntity.Shape.Quad(FloatSize2d(1f, 1f))
 
     lateinit var underTest: FakeSurfaceEntity
 
@@ -57,7 +58,7 @@
 
     @Test
     fun getDimensions_setCanvasShape_returnsCanvasShapeDimensions() {
-        underTest.canvasShape = testCanvasShape
+        underTest.shape = testCanvasShape
 
         assertThat(underTest.dimensions).isEqualTo(testCanvasShape.dimensions)
     }
@@ -123,7 +124,7 @@
         check(underTest.colorSpace == SurfaceEntity.ColorSpace.BT709)
         check(underTest.colorTransfer == SurfaceEntity.ColorTransfer.LINEAR)
         check(underTest.colorRange == SurfaceEntity.ColorRange.FULL)
-        check(underTest.maxCLL == 0)
+        check(underTest.maxContentLightLevel == 0)
 
         underTest.setContentColorMetadata(
             SurfaceEntity.ColorSpace.BT2020,
@@ -135,7 +136,7 @@
         assertThat(underTest.colorSpace).isEqualTo(SurfaceEntity.ColorSpace.BT2020)
         assertThat(underTest.colorTransfer).isEqualTo(SurfaceEntity.ColorTransfer.SRGB)
         assertThat(underTest.colorRange).isEqualTo(SurfaceEntity.ColorRange.LIMITED)
-        assertThat(underTest.maxCLL).isEqualTo(100)
+        assertThat(underTest.maxContentLightLevel).isEqualTo(100)
     }
 
     @Test
@@ -150,13 +151,13 @@
         assertThat(underTest.colorSpace).isEqualTo(SurfaceEntity.ColorSpace.BT2020)
         assertThat(underTest.colorTransfer).isEqualTo(SurfaceEntity.ColorTransfer.SRGB)
         assertThat(underTest.colorRange).isEqualTo(SurfaceEntity.ColorRange.LIMITED)
-        assertThat(underTest.maxCLL).isEqualTo(100)
+        assertThat(underTest.maxContentLightLevel).isEqualTo(100)
 
         underTest.resetContentColorMetadata()
 
         assertThat(underTest.colorSpace).isEqualTo(SurfaceEntity.ColorSpace.BT709)
         assertThat(underTest.colorTransfer).isEqualTo(SurfaceEntity.ColorTransfer.LINEAR)
         assertThat(underTest.colorRange).isEqualTo(SurfaceEntity.ColorRange.FULL)
-        assertThat(underTest.maxCLL).isEqualTo(0)
+        assertThat(underTest.maxContentLightLevel).isEqualTo(0)
     }
 }
diff --git a/xr/runtime/runtime/api/restricted_current.txt b/xr/runtime/runtime/api/restricted_current.txt
index af071ff..c9a3e97 100644
--- a/xr/runtime/runtime/api/restricted_current.txt
+++ b/xr/runtime/runtime/api/restricted_current.txt
@@ -1025,7 +1025,7 @@
     method public androidx.xr.runtime.internal.ResizableComponent createResizableComponent(androidx.xr.runtime.internal.Dimensions minimumSize, androidx.xr.runtime.internal.Dimensions maximumSize);
     method public androidx.xr.runtime.internal.SpatialPointerComponent createSpatialPointerComponent();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.runtime.internal.SubspaceNodeEntity createSubspaceNodeEntity(androidx.xr.runtime.SubspaceNodeHolder<? extends java.lang.Object?> subspaceNodeHolder, androidx.xr.runtime.internal.Dimensions size);
-    method public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(int stereoMode, androidx.xr.runtime.math.Pose pose, androidx.xr.runtime.internal.SurfaceEntity.CanvasShape canvasShape, @androidx.xr.runtime.internal.SurfaceEntity.ContentSecurityLevel int contentSecurityLevel, int superSampling, androidx.xr.runtime.internal.Entity parentEntity);
+    method public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(int stereoMode, androidx.xr.runtime.math.Pose pose, androidx.xr.runtime.internal.SurfaceEntity.Shape shape, @androidx.xr.runtime.internal.SurfaceEntity.SurfaceProtection int surfaceProtection, int superSampling, androidx.xr.runtime.internal.Entity parentEntity);
     method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.MaterialResource> createWaterMaterial(boolean isAlphaMapVersion);
     method public void destroyKhronosPbrMaterial(androidx.xr.runtime.internal.MaterialResource material);
     method public void destroyTexture(androidx.xr.runtime.internal.TextureResource texture);
@@ -1428,6 +1428,11 @@
     method public void destroyTexture(androidx.xr.runtime.internal.TextureResource texture);
     method public void destroyWaterMaterial(androidx.xr.runtime.internal.MaterialResource material);
     method public void dispose();
+    method public androidx.xr.runtime.internal.TextureResource? getReflectionTextureFromIbl(androidx.xr.runtime.internal.ExrImageResource iblToken);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.ExrImageResource> loadExrImageByAssetName(String assetName);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.ExrImageResource> loadExrImageByByteArray(byte[] assetData, String assetKey);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.GltfModelResource> loadGltfByAssetName(String assetName);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.GltfModelResource> loadGltfByByteArray(byte[] assetData, String assetKey);
     method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.TextureResource> loadTexture(String assetName, androidx.xr.runtime.internal.TextureSampler sampler);
     method public void setAlphaCutoffOnKhronosPbrMaterial(androidx.xr.runtime.internal.MaterialResource material, float alphaCutoff);
     method public void setAlphaMapOnWaterMaterial(androidx.xr.runtime.internal.MaterialResource material, androidx.xr.runtime.internal.TextureResource alphaMap);
@@ -1747,67 +1752,36 @@
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SurfaceEntity extends androidx.xr.runtime.internal.Entity {
-    method public androidx.xr.runtime.internal.SurfaceEntity.CanvasShape getCanvasShape();
     method public int getColorRange();
     method public int getColorSpace();
     method public int getColorTransfer();
     method public boolean getContentColorMetadataSet();
     method public androidx.xr.runtime.internal.Dimensions getDimensions();
     method public androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather getEdgeFeather();
-    method public int getMaxCLL();
+    method public int getMaxContentLightLevel();
     method public androidx.xr.runtime.internal.PerceivedResolutionResult getPerceivedResolution();
+    method public androidx.xr.runtime.internal.SurfaceEntity.Shape getShape();
     method public int getStereoMode();
     method public android.view.Surface getSurface();
     method public void resetContentColorMetadata();
     method public void setAuxiliaryAlphaMaskTexture(androidx.xr.runtime.internal.TextureResource? alphaMask);
-    method public void setCanvasShape(androidx.xr.runtime.internal.SurfaceEntity.CanvasShape);
-    method public void setContentColorMetadata(int colorSpace, int colorTransfer, int colorRange, int maxCLL);
+    method public void setContentColorMetadata(int colorSpace, int colorTransfer, int colorRange, int maxContentLightLevel);
     method public void setEdgeFeather(androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather);
     method public void setPrimaryAlphaMaskTexture(androidx.xr.runtime.internal.TextureResource? alphaMask);
+    method public void setShape(androidx.xr.runtime.internal.SurfaceEntity.Shape);
     method public void setStereoMode(int);
-    property public abstract androidx.xr.runtime.internal.SurfaceEntity.CanvasShape canvasShape;
     property public abstract int colorRange;
     property public abstract int colorSpace;
     property public abstract int colorTransfer;
     property public abstract boolean contentColorMetadataSet;
     property public abstract androidx.xr.runtime.internal.Dimensions dimensions;
     property public abstract androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather edgeFeather;
-    property public abstract int maxCLL;
+    property public abstract int maxContentLightLevel;
+    property public abstract androidx.xr.runtime.internal.SurfaceEntity.Shape shape;
     property public abstract int stereoMode;
     property public abstract android.view.Surface surface;
   }
 
-  public static interface SurfaceEntity.CanvasShape {
-    method public androidx.xr.runtime.internal.Dimensions getDimensions();
-    property public abstract androidx.xr.runtime.internal.Dimensions dimensions;
-  }
-
-  public static final class SurfaceEntity.CanvasShape.Quad implements androidx.xr.runtime.internal.SurfaceEntity.CanvasShape {
-    ctor public SurfaceEntity.CanvasShape.Quad(float width, float height);
-    method public androidx.xr.runtime.internal.Dimensions getDimensions();
-    method public float getHeight();
-    method public float getWidth();
-    property public androidx.xr.runtime.internal.Dimensions dimensions;
-    property public float height;
-    property public float width;
-  }
-
-  public static final class SurfaceEntity.CanvasShape.Vr180Hemisphere implements androidx.xr.runtime.internal.SurfaceEntity.CanvasShape {
-    ctor public SurfaceEntity.CanvasShape.Vr180Hemisphere(float radius);
-    method public androidx.xr.runtime.internal.Dimensions getDimensions();
-    method public float getRadius();
-    property public androidx.xr.runtime.internal.Dimensions dimensions;
-    property public float radius;
-  }
-
-  public static final class SurfaceEntity.CanvasShape.Vr360Sphere implements androidx.xr.runtime.internal.SurfaceEntity.CanvasShape {
-    ctor public SurfaceEntity.CanvasShape.Vr360Sphere(float radius);
-    method public androidx.xr.runtime.internal.Dimensions getDimensions();
-    method public float getRadius();
-    property public androidx.xr.runtime.internal.Dimensions dimensions;
-    property public float radius;
-  }
-
   @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public static @interface SurfaceEntity.ColorRange {
     field public static final androidx.xr.runtime.internal.SurfaceEntity.ColorRange.Companion Companion;
     field public static final int FULL = 1; // 0x1
@@ -1874,32 +1848,48 @@
     field public static final int ST2084 = 6; // 0x6
   }
 
-  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public static @interface SurfaceEntity.ContentSecurityLevel {
-    field public static final androidx.xr.runtime.internal.SurfaceEntity.ContentSecurityLevel.Companion Companion;
-    field public static final int NONE = 0; // 0x0
-    field public static final int PROTECTED = 1; // 0x1
-  }
-
-  public static final class SurfaceEntity.ContentSecurityLevel.Companion {
-    property public static int NONE;
-    property public static int PROTECTED;
-    field public static final int NONE = 0; // 0x0
-    field public static final int PROTECTED = 1; // 0x1
-  }
-
   public static interface SurfaceEntity.EdgeFeather {
   }
 
-  public static final class SurfaceEntity.EdgeFeather.SmoothFeather implements androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather {
-    ctor public SurfaceEntity.EdgeFeather.SmoothFeather(float leftRight, float topBottom);
+  public static final class SurfaceEntity.EdgeFeather.NoFeathering implements androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather {
+    ctor public SurfaceEntity.EdgeFeather.NoFeathering();
+  }
+
+  public static final class SurfaceEntity.EdgeFeather.RectangleFeather implements androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather {
+    ctor public SurfaceEntity.EdgeFeather.RectangleFeather(float leftRight, float topBottom);
     method public float getLeftRight();
     method public float getTopBottom();
     property public float leftRight;
     property public float topBottom;
   }
 
-  public static final class SurfaceEntity.EdgeFeather.SolidEdge implements androidx.xr.runtime.internal.SurfaceEntity.EdgeFeather {
-    ctor public SurfaceEntity.EdgeFeather.SolidEdge();
+  public static interface SurfaceEntity.Shape {
+    method public androidx.xr.runtime.internal.Dimensions getDimensions();
+    property public abstract androidx.xr.runtime.internal.Dimensions dimensions;
+  }
+
+  public static final class SurfaceEntity.Shape.Hemisphere implements androidx.xr.runtime.internal.SurfaceEntity.Shape {
+    ctor public SurfaceEntity.Shape.Hemisphere(float radius);
+    method public androidx.xr.runtime.internal.Dimensions getDimensions();
+    method public float getRadius();
+    property public androidx.xr.runtime.internal.Dimensions dimensions;
+    property public float radius;
+  }
+
+  public static final class SurfaceEntity.Shape.Quad implements androidx.xr.runtime.internal.SurfaceEntity.Shape {
+    ctor public SurfaceEntity.Shape.Quad(androidx.xr.runtime.math.FloatSize2d extents);
+    method public androidx.xr.runtime.internal.Dimensions getDimensions();
+    method public androidx.xr.runtime.math.FloatSize2d getExtents();
+    property public androidx.xr.runtime.internal.Dimensions dimensions;
+    property public androidx.xr.runtime.math.FloatSize2d extents;
+  }
+
+  public static final class SurfaceEntity.Shape.Sphere implements androidx.xr.runtime.internal.SurfaceEntity.Shape {
+    ctor public SurfaceEntity.Shape.Sphere(float radius);
+    method public androidx.xr.runtime.internal.Dimensions getDimensions();
+    method public float getRadius();
+    property public androidx.xr.runtime.internal.Dimensions dimensions;
+    property public float radius;
   }
 
   @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public static @interface SurfaceEntity.StereoMode {
@@ -1937,6 +1927,19 @@
     field public static final int NONE = 0; // 0x0
   }
 
+  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public static @interface SurfaceEntity.SurfaceProtection {
+    field public static final androidx.xr.runtime.internal.SurfaceEntity.SurfaceProtection.Companion Companion;
+    field public static final int NONE = 0; // 0x0
+    field public static final int PROTECTED = 1; // 0x1
+  }
+
+  public static final class SurfaceEntity.SurfaceProtection.Companion {
+    property public static int NONE;
+    property public static int PROTECTED;
+    field public static final int NONE = 0; // 0x0
+    field public static final int PROTECTED = 1; // 0x1
+  }
+
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface SystemSpaceEntity extends androidx.xr.runtime.internal.Entity {
     method public void setOnSpaceUpdatedListener(Runnable? listener, java.util.concurrent.Executor? executor);
   }
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/JxrPlatformAdapter.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/JxrPlatformAdapter.kt
index a2e46a6..71dd832 100644
--- a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/JxrPlatformAdapter.kt
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/JxrPlatformAdapter.kt
@@ -444,10 +444,9 @@
      *
      * @param stereoMode Stereo mode for the surface.
      * @param pose Pose of this entity relative to its parent, default value is Identity.
-     * @param canvasShape The [SurfaceEntity.CanvasShape] which describes the spatialized shape of
-     *   the canvas.
-     * @param contentSecurityLevel The [SurfaceEntity.ContentSecurityLevel] which describes whether
-     *   DRM is enabled.
+     * @param shape The [SurfaceEntity.Shape] which describes the 3D geometry of the entity.
+     * @param surfaceProtection The [SurfaceEntity.SurfaceProtection] which describes whether DRM is
+     *   enabled.
      * @param superSampling The [SurfaceEntity.SuperSampling] which describes whether super sampling
      *   is enabled. Whether to use super sampling for the surface.
      * @param parentEntity The parent entity of this entity.
@@ -456,8 +455,8 @@
     public fun createSurfaceEntity(
         stereoMode: Int,
         pose: Pose,
-        canvasShape: SurfaceEntity.CanvasShape,
-        @SurfaceEntity.ContentSecurityLevel contentSecurityLevel: Int,
+        shape: SurfaceEntity.Shape,
+        @SurfaceEntity.SurfaceProtection surfaceProtection: Int,
         superSampling: Int,
         parentEntity: Entity,
     ): SurfaceEntity
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/RenderingRuntime.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/RenderingRuntime.kt
index 4eff280..e51fe76 100644
--- a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/RenderingRuntime.kt
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/RenderingRuntime.kt
@@ -36,6 +36,59 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 public interface RenderingRuntime {
     /**
+     * Loads glTF Asset for the given asset name from the assets folder. The future returned by this
+     * method will fire listeners on the UI thread if Runnable::run is supplied.
+     *
+     * @param assetName The name of the asset to load from the assets folder.
+     * @return A future that resolves to the glTF model when it is loaded. The future will be null
+     *   if the asset was not found.
+     */
+    @Suppress("AsyncSuffixFuture")
+    public fun loadGltfByAssetName(assetName: String): ListenableFuture<GltfModelResource>
+
+    /**
+     * Loads glTF Asset from a provided byte array. The future returned by this method will fire
+     * listeners on the UI thread if Runnable::run is supplied.
+     *
+     * @param assetData A gltfAsset in the form of a byte array.
+     * @param assetKey The name of the asset to load from the cache.
+     * @return A future that resolves to the glTF model when it is loaded. The future will be null
+     *   if the asset was not found.
+     */
+    @Suppress("AsyncSuffixFuture")
+    // TODO(b/397746548): Add InputStream support for loading glTFs.
+    // Suppressed to allow CompletableFuture.
+    public fun loadGltfByByteArray(
+        assetData: ByteArray,
+        assetKey: String,
+    ): ListenableFuture<GltfModelResource>
+
+    /**
+     * Loads an ExrImage for the given asset name from the assets folder.
+     *
+     * @param assetName The name of the asset to load from the assets folder.
+     * @return A future that resolves to the ExrImage when it is loaded. The future will be null if
+     *   the asset was not found.
+     */
+    @SuppressWarnings("AsyncSuffixFuture")
+    public fun loadExrImageByAssetName(assetName: String): ListenableFuture<ExrImageResource>
+
+    /**
+     * Loads an ExrImage from a provided byte array.
+     *
+     * @param assetData An ExrImage in the form of a byte array.
+     * @param assetKey The name of the asset to load from the cache.
+     * @return A future that resolves to the ExrImage when it is loaded. The future will be null if
+     *   the asset was not found.
+     */
+    @Suppress("AsyncSuffixFuture")
+    // Suppressed to allow CompletableFuture.
+    public fun loadExrImageByByteArray(
+        assetData: ByteArray,
+        assetKey: String,
+    ): ListenableFuture<ExrImageResource>
+
+    /**
      * Loads a texture resource for the given asset name or URL. The future returned by this method
      * will fire listeners on the UI thread if Runnable::run is supplied.
      *
@@ -60,6 +113,15 @@
     public fun destroyTexture(texture: TextureResource)
 
     /**
+     * Returns the reflection texture from the given IBL.
+     *
+     * @param iblToken An ExrImageResource representing a loaded Image-Based Lighting (IBL) asset.
+     * @return A TextureResource representing the reflection texture, or null if the texture was not
+     *   found.
+     */
+    public fun getReflectionTextureFromIbl(iblToken: ExrImageResource): TextureResource?
+
+    /**
      * Creates a water material by querying it from the system's built-in materials. The future
      * returned by this method will fire listeners on the UI thread if Runnable::run is supplied.
      *
diff --git a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/SurfaceEntity.kt b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/SurfaceEntity.kt
index d28d461..d0cc6ad 100644
--- a/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/SurfaceEntity.kt
+++ b/xr/runtime/runtime/src/main/kotlin/androidx/xr/runtime/internal/SurfaceEntity.kt
@@ -18,6 +18,7 @@
 
 import android.view.Surface
 import androidx.annotation.RestrictTo
+import androidx.xr.runtime.math.FloatSize2d
 
 /**
  * Interface for a spatialized Entity which manages an Android Surface. Applications can render to
@@ -38,11 +39,11 @@
     public var stereoMode: Int
 
     /**
-     * Specifies the shape of the spatial canvas which the surface is texture mapped to.
+     * Specifies the geometry of the spatial canvas which the surface is texture mapped to.
      *
      * @throws IllegalStateException when setting this value if the Entity has been disposed.
      */
-    public var canvasShape: CanvasShape
+    public var shape: Shape
 
     /**
      * Retrieves the dimensions of the "spatial canvas" which the surface is mapped to. These values
@@ -103,8 +104,8 @@
      * Indicates whether explicit color information has been set for the surface content. If
      * `false`, the runtime should signal the backend to use its best effort color correction and
      * tonemapping. If `true`, the runtime should inform the backend to use the values specified in
-     * [colorSpace], [colorTransfer], [colorRange], and [maxCLL] for color correction and
-     * tonemapping of the surface content.
+     * [colorSpace], [colorTransfer], [colorRange], and [maxContentLightLevel] for color correction
+     * and tonemapping of the surface content.
      *
      * This property is typically managed by the `setContentColorMetadata` and
      * `resetContentColorMetadata` methods.
@@ -131,10 +132,11 @@
     public val colorRange: Int
 
     /**
-     * The active maximum content light level (MaxCLL) in nits. A value of 0 indicates that MaxCLL
-     * is not set or is unknown. This value is used if [contentColorMetadataSet] is `true`.
+     * The active maximum content light level (MaxContentLightLevel) in nits. A value of 0 indicates
+     * that MaxContentLightLevel is not set or is unknown. This value is used if
+     * [contentColorMetadataSet] is `true`.
      */
-    public val maxCLL: Int
+    public val maxContentLightLevel: Int
 
     /**
      * Sets the explicit color information for the surface content. This will also set
@@ -144,20 +146,20 @@
      * @param colorTransfer The runtime color transfer value (e.g.,
      *   [SurfaceEntity.ColorTransfer.SRGB]).
      * @param colorRange The runtime color range value (e.g., [SurfaceEntity.ColorRange.FULL]).
-     * @param maxCLL The maximum content light level in nits.
+     * @param maxContentLightLevel The maximum content light level in nits.
      * @throws IllegalStateException if the Entity has been disposed.
      */
     public fun setContentColorMetadata(
         colorSpace: Int,
         colorTransfer: Int,
         colorRange: Int,
-        maxCLL: Int,
+        maxContentLightLevel: Int,
     )
 
     /**
      * Resets the color information to the runtime's default handling. This will set
      * [contentColorMetadataSet] to `false` and typically involves reverting [colorSpace],
-     * [colorTransfer], [colorRange], and [maxCLL] to their default runtime values.
+     * [colorTransfer], [colorRange], and [maxContentLightLevel] to their default runtime values.
      *
      * @throws IllegalStateException if the Entity has been disposed.
      */
@@ -189,7 +191,7 @@
      *
      * See https://developer.android.com/reference/android/media/MediaDrm for more details.
      */
-    public annotation class ContentSecurityLevel {
+    public annotation class SurfaceProtection {
         public companion object {
             // The Surface content is not secured. DRM content can not be decoded into this Surface.
             // Screen captures of the SurfaceEntity will show the Surface content.
@@ -264,22 +266,22 @@
     }
 
     /** Represents the shape of the spatial canvas which the surface is texture mapped to. */
-    public interface CanvasShape {
+    public interface Shape {
         public val dimensions: Dimensions
 
         /**
          * A 2D rectangle-shaped canvas. Width and height are represented in the local spatial
          * coordinate system of the entity. (0,0,0) is the center of the canvas.
          */
-        public class Quad(public val width: Float, public val height: Float) : CanvasShape {
-            override val dimensions: Dimensions = Dimensions(width, height, 0f)
+        public class Quad(public val extents: FloatSize2d) : Shape {
+            override val dimensions: Dimensions = Dimensions(extents.width, extents.height, 0f)
         }
 
         /**
          * A sphere-shaped canvas. Radius is represented in the local spatial coordinate system of
          * the entity. (0,0,0) is the center of the sphere.
          */
-        public class Vr360Sphere(public val radius: Float) : CanvasShape {
+        public class Sphere(public val radius: Float) : Shape {
             override val dimensions: Dimensions = Dimensions(radius * 2, radius * 2, radius * 2)
         }
 
@@ -287,7 +289,7 @@
          * A hemisphere-shaped canvas. Radius is represented in the local spatial coordinate system
          * of the entity. (0,0,0) is the center of the base of the hemisphere.
          */
-        public class Vr180Hemisphere(public val radius: Float) : CanvasShape {
+        public class Hemisphere(public val radius: Float) : Shape {
             override val dimensions: Dimensions = Dimensions(radius * 2, radius * 2, radius)
         }
     }
@@ -295,15 +297,15 @@
     /** Specifies edge transparency effects for the canvas. */
     public interface EdgeFeather {
         /** A smooth feathering effect which moves from edges of UV space to the center. */
-        public class SmoothFeather(public val leftRight: Float, public val topBottom: Float) :
+        public class RectangleFeather(public val leftRight: Float, public val topBottom: Float) :
             EdgeFeather
 
         /** A Default implementation of EdgeFeather that does nothing. */
-        public class SolidEdge() : EdgeFeather
+        public class NoFeathering() : EdgeFeather
     }
 
     /**
-     * The edge feathering effect for the spatialized geometry.
+     * The edge feathering effect for the spatialized shape.
      *
      * @throws IllegalStateException if the Entity has been disposed.
      */
diff --git a/xr/scenecore/integration-tests/fieldofviewvisibility/src/main/kotlin/androidx/xr/scenecore/samples/fieldofviewvisibility/SurfaceEntityManager.kt b/xr/scenecore/integration-tests/fieldofviewvisibility/src/main/kotlin/androidx/xr/scenecore/samples/fieldofviewvisibility/SurfaceEntityManager.kt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/xr/scenecore/integration-tests/fieldofviewvisibility/src/main/kotlin/androidx/xr/scenecore/samples/fieldofviewvisibility/SurfaceEntityManager.kt
diff --git a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/activitypanel/ActivityPanelActivity.kt b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/activitypanel/ActivityPanelActivity.kt
index 85bb1e7..a86e30a 100644
--- a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/activitypanel/ActivityPanelActivity.kt
+++ b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/activitypanel/ActivityPanelActivity.kt
@@ -96,7 +96,7 @@
                     val intent = Intent(this, ActivityPanel::class.java)
                     intent.putExtra("NAV_ICON", false)
                     // Launch an activity in the panel
-                    activityPanelEntity.launchActivity(intent, savedInstanceState)
+                    activityPanelEntity.startActivity(intent, savedInstanceState)
                     // Add movable component
                     val movableComponent = MovableComponent.createSystemMovable(session!!)
                     activityPanelEntity.addComponent(movableComponent)
diff --git a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/common/managers/SurfaceEntityManager.kt b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/common/managers/SurfaceEntityManager.kt
index 1206de6..1bea6ff 100644
--- a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/common/managers/SurfaceEntityManager.kt
+++ b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/common/managers/SurfaceEntityManager.kt
@@ -20,6 +20,7 @@
 import android.widget.RadioGroup
 import androidx.appcompat.app.AppCompatActivity
 import androidx.xr.runtime.Session
+import androidx.xr.runtime.math.FloatSize2d
 import androidx.xr.runtime.math.FloatSize3d
 import androidx.xr.runtime.math.Pose
 import androidx.xr.scenecore.MovableComponent
@@ -47,21 +48,21 @@
         activity.findViewById<Button>(R.id.button_destroy_surface_entity)
     val canvasRadioOptions =
         listOf(
-            SurfaceEntity.CanvasShape.Quad(1f, 1f),
-            SurfaceEntity.CanvasShape.Vr180Hemisphere(1f),
-            SurfaceEntity.CanvasShape.Vr360Sphere(1f),
+            SurfaceEntity.Shape.Quad(FloatSize2d(1f, 1f)),
+            SurfaceEntity.Shape.Hemisphere(1f),
+            SurfaceEntity.Shape.Sphere(1f),
         )
-    private val _selectedCanvasShapeOptionFlow = MutableStateFlow(canvasRadioOptions[0])
-    private var selectedCanvasShapeOption: SurfaceEntity.CanvasShape
-        get() = _selectedCanvasShapeOptionFlow.value
+    private val _selectedShapeOptionFlow = MutableStateFlow(canvasRadioOptions[0])
+    private var selectedShapeOption: SurfaceEntity.Shape
+        get() = _selectedShapeOptionFlow.value
         set(value) {
-            _selectedCanvasShapeOptionFlow.value = value
+            _selectedShapeOptionFlow.value = value
         }
 
     init {
         updateButtonStates()
         surfaceEntityRadioGroup.setOnCheckedChangeListener { group, checkedId ->
-            selectedCanvasShapeOption =
+            selectedShapeOption =
                 when (checkedId) {
                     R.id.radiobutton_quad -> canvasRadioOptions[0]
                     R.id.radiobutton_vr180 -> canvasRadioOptions[1]
@@ -70,7 +71,7 @@
                 }
             // If entity exists, update its shape immediately
             if (surfaceEntity != null) {
-                surfaceEntity?.canvasShape = selectedCanvasShapeOption
+                surfaceEntity?.shape = selectedShapeOption
             }
         }
 
@@ -90,10 +91,10 @@
         if (surfaceEntity == null) {
             surfaceEntity =
                 SurfaceEntity.create(
-                    session,
-                    SurfaceEntity.StereoMode.MONO,
-                    Pose.Identity,
-                    selectedCanvasShapeOption,
+                    session = session,
+                    pose = Pose.Identity,
+                    shape = selectedShapeOption,
+                    stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO,
                 )
             // Make the video player movable (to make it easier to look at it from
             // different angles and distances)
diff --git a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/panelroundedcorner/PanelRoundedCornerActivity.kt b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/panelroundedcorner/PanelRoundedCornerActivity.kt
index b8ef940..5f19f0a 100644
--- a/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/panelroundedcorner/PanelRoundedCornerActivity.kt
+++ b/xr/scenecore/integration-tests/testapp/src/main/kotlin/androidx/xr/scenecore/testapp/panelroundedcorner/PanelRoundedCornerActivity.kt
@@ -170,7 +170,7 @@
                 ActivityPanelEntity.create(session!!, IntSize2d(640, 480), "activity_panel")
             val intent = Intent(this, ActivityPanel::class.java)
             intent.putExtra("NAV_ICON", false)
-            activityPanelEntity!!.launchActivity(intent)
+            activityPanelEntity!!.startActivity(intent)
             activityPanelEntity!!.setPose(Pose(Vector3(0.75f, 0.0f, 0.0f)))
             activityPanelCreated = true
         }
diff --git a/xr/scenecore/integration-tests/videoplayerdrmtest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayerdrmtest/VideoPlayerDrmTestActivity.kt b/xr/scenecore/integration-tests/videoplayerdrmtest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayerdrmtest/VideoPlayerDrmTestActivity.kt
index 4c7f83c..983319a 100644
--- a/xr/scenecore/integration-tests/videoplayerdrmtest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayerdrmtest/VideoPlayerDrmTestActivity.kt
+++ b/xr/scenecore/integration-tests/videoplayerdrmtest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayerdrmtest/VideoPlayerDrmTestActivity.kt
@@ -68,6 +68,7 @@
 import androidx.xr.runtime.Config.HeadTrackingMode
 import androidx.xr.runtime.Session
 import androidx.xr.runtime.SessionCreateSuccess
+import androidx.xr.runtime.math.FloatSize2d
 import androidx.xr.runtime.math.FloatSize3d
 import androidx.xr.runtime.math.IntSize2d
 import androidx.xr.runtime.math.Pose
@@ -229,13 +230,13 @@
 
     fun getCanvasAspectRatio(stereoMode: Int, videoWidth: Int, videoHeight: Int): FloatSize3d {
         when (stereoMode) {
-            SurfaceEntity.StereoMode.MONO,
-            SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
-            SurfaceEntity.StereoMode.MULTIVIEW_RIGHT_PRIMARY ->
+            SurfaceEntity.StereoMode.STEREO_MODE_MONO,
+            SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
+            SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY ->
                 return FloatSize3d(1.0f, videoHeight.toFloat() / videoWidth, 0.0f)
-            SurfaceEntity.StereoMode.TOP_BOTTOM ->
+            SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM ->
                 return FloatSize3d(1.0f, 0.5f * videoHeight.toFloat() / videoWidth, 0.0f)
-            SurfaceEntity.StereoMode.SIDE_BY_SIDE ->
+            SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE ->
                 return FloatSize3d(1.0f, 2.0f * videoHeight.toFloat() / videoWidth, 0.0f)
             else -> throw IllegalArgumentException("Unsupported stereo mode: $stereoMode")
         }
@@ -246,7 +247,7 @@
         videoUri: String,
         stereoMode: Int,
         pose: Pose,
-        canvasShape: SurfaceEntity.CanvasShape,
+        shape: SurfaceEntity.Shape,
         loop: Boolean = true,
         protected: Boolean = false,
     ) {
@@ -254,20 +255,26 @@
         if (surfaceEntity == null) {
             val surfaceContentLevel =
                 if (protected) {
-                    SurfaceEntity.ContentSecurityLevel.PROTECTED
+                    SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_PROTECTED
                 } else {
-                    SurfaceEntity.ContentSecurityLevel.NONE
+                    SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_NONE
                 }
 
             surfaceEntity =
-                SurfaceEntity.create(session, stereoMode, pose, canvasShape, surfaceContentLevel)
+                SurfaceEntity.create(
+                    session = session,
+                    pose = pose,
+                    shape = shape,
+                    stereoMode = stereoMode,
+                    surfaceProtection = surfaceContentLevel,
+                )
             // Make the video player movable (to make it easier to look at it from different
             // angles and distances) (only on quad canvas)
             movableComponent = MovableComponent.createSystemMovable(session)
             // The quad has a radius of 1.0 meters
             movableComponent!!.size = FloatSize3d(1.0f, 1.0f, 1.0f)
 
-            if (canvasShape is SurfaceEntity.CanvasShape.Quad) {
+            if (shape is SurfaceEntity.Shape.Quad) {
                 val unused = surfaceEntity!!.addComponent(movableComponent!!)
             }
         }
@@ -306,10 +313,14 @@
                     // Resize the canvas to match the video aspect ratio - accounting for
                     // the stereo mode.
                     val dimensions = getCanvasAspectRatio(stereoMode, width, height)
-                    // Set the dimensions of the Quad canvas to the video dimensions
-                    if (canvasShape is SurfaceEntity.CanvasShape.Quad) {
-                        surfaceEntity?.canvasShape =
-                            SurfaceEntity.CanvasShape.Quad(dimensions.width, dimensions.height)
+                    // Set the dimensions of the Quad canvas to the video dimensions and
+                    // attach the
+                    // a MovableComponent.
+                    if (shape is SurfaceEntity.Shape.Quad) {
+                        surfaceEntity?.shape =
+                            SurfaceEntity.Shape.Quad(
+                                FloatSize2d(dimensions.width, dimensions.height)
+                            )
                         movableComponent?.size =
                             surfaceEntity?.dimensions ?: FloatSize3d(1.0f, 1.0f, 1.0f)
                     }
@@ -392,7 +403,7 @@
         videoUri: String,
         stereoMode: Int,
         pose: Pose,
-        canvasShape: SurfaceEntity.CanvasShape,
+        shape: SurfaceEntity.Shape,
         buttonText: String,
         enabled: Boolean = true,
         loop: Boolean = true,
@@ -414,7 +425,7 @@
             enabled = enabled,
             onClick = {
                 // Create SurfaceEntity and MovableComponent if they don't exist.
-                playVideo(session, videoUri, stereoMode, pose, canvasShape, loop, protected)
+                playVideo(session, videoUri, stereoMode, pose, shape, loop, protected)
             },
         ) {
             Text(text = buttonText, fontSize = 20.sp)
@@ -452,9 +463,9 @@
             playVideo(
                 session = session,
                 videoUri = videoUri,
-                stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM,
+                stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM,
                 pose = Pose(Vector3(0.0f, 0.0f, -0.25f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
-                canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+                shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
                 loop = true,
                 protected = false,
             )
@@ -499,9 +510,9 @@
             playVideo(
                 session = session,
                 videoUri = videoUri,
-                stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE,
+                stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE,
                 pose = Pose(Vector3(0.0f, 0.0f, -0.25f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
-                canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+                shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
                 loop = true,
                 protected = true,
             )
diff --git a/xr/scenecore/integration-tests/videoplayertest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayertest/VideoPlayerTestActivity.kt b/xr/scenecore/integration-tests/videoplayertest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayertest/VideoPlayerTestActivity.kt
index 0a399de..c605581 100644
--- a/xr/scenecore/integration-tests/videoplayertest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayertest/VideoPlayerTestActivity.kt
+++ b/xr/scenecore/integration-tests/videoplayertest/src/main/kotlin/androidx/xr/scenecore/samples/videoplayertest/VideoPlayerTestActivity.kt
@@ -83,6 +83,7 @@
 import androidx.xr.runtime.Config.HeadTrackingMode
 import androidx.xr.runtime.Session
 import androidx.xr.runtime.SessionCreateSuccess
+import androidx.xr.runtime.math.FloatSize2d
 import androidx.xr.runtime.math.FloatSize3d
 import androidx.xr.runtime.math.IntSize2d
 import androidx.xr.runtime.math.Pose
@@ -330,13 +331,13 @@
         val effectiveDisplayWidth = videoWidth.toFloat() * pixelAspectRatio
 
         return when (stereoMode) {
-            SurfaceEntity.StereoMode.MONO,
-            SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
-            SurfaceEntity.StereoMode.MULTIVIEW_RIGHT_PRIMARY ->
+            SurfaceEntity.StereoMode.STEREO_MODE_MONO,
+            SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
+            SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY ->
                 FloatSize3d(1.0f, videoHeight.toFloat() / effectiveDisplayWidth, 0.0f)
-            SurfaceEntity.StereoMode.TOP_BOTTOM ->
+            SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM ->
                 FloatSize3d(1.0f, 0.5f * videoHeight.toFloat() / effectiveDisplayWidth, 0.0f)
-            SurfaceEntity.StereoMode.SIDE_BY_SIDE ->
+            SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE ->
                 FloatSize3d(1.0f, 2.0f * videoHeight.toFloat() / effectiveDisplayWidth, 0.0f)
             else -> throw IllegalArgumentException("Unsupported stereo mode: $stereoMode")
         }
@@ -413,8 +414,8 @@
                         value = featherRadiusX,
                         onValueChange = {
                             featherRadiusX = it
-                            surfaceEntity!!.edgeFeather =
-                                SurfaceEntity.EdgeFeatheringParams.SmoothFeather(
+                            surfaceEntity!!.edgeFeatheringParams =
+                                SurfaceEntity.EdgeFeatheringParams.RectangleFeather(
                                     featherRadiusX,
                                     featherRadiusY,
                                 )
@@ -425,8 +426,8 @@
                         value = featherRadiusY,
                         onValueChange = {
                             featherRadiusY = it
-                            surfaceEntity!!.edgeFeather =
-                                SurfaceEntity.EdgeFeatheringParams.SmoothFeather(
+                            surfaceEntity!!.edgeFeatheringParams =
+                                SurfaceEntity.EdgeFeatheringParams.RectangleFeather(
                                     featherRadiusX,
                                     featherRadiusY,
                                 )
@@ -438,7 +439,7 @@
             Row(verticalAlignment = Alignment.CenterVertically) {
                 Button(
                     onClick = {
-                        surfaceEntity!!.canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f)
+                        surfaceEntity!!.shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f))
                         // Move the Quad-shaped canvas to a spot in front of the User.
                         surfaceEntity!!.setPose(
                             session.scene.spatialUser.head?.transformPoseTo(
@@ -453,33 +454,33 @@
                 ) {
                     Text(text = "Set Quad", fontSize = 10.sp)
                 }
-                Button(
-                    onClick = {
-                        surfaceEntity!!.canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(1.0f)
-                    }
-                ) {
+                Button(onClick = { surfaceEntity!!.shape = SurfaceEntity.Shape.Sphere(1.0f) }) {
                     Text(text = "Set Vr360", fontSize = 10.sp)
                 }
-                Button(
-                    onClick = {
-                        surfaceEntity!!.canvasShape =
-                            SurfaceEntity.CanvasShape.Vr180Hemisphere(1.0f)
-                    }
-                ) {
+                Button(onClick = { surfaceEntity!!.shape = SurfaceEntity.Shape.Hemisphere(1.0f) }) {
                     Text(text = "Set Vr180", fontSize = 10.sp)
                 }
             } // end row
             Row(verticalAlignment = Alignment.CenterVertically) {
-                Button(onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.MONO }) {
+                Button(
+                    onClick = {
+                        surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO
+                    }
+                ) {
                     Text(text = "Mono", fontSize = 10.sp)
                 }
                 Button(
-                    onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM }
+                    onClick = {
+                        surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM
+                    }
                 ) {
                     Text(text = "Top-Bottom", fontSize = 10.sp)
                 }
                 Button(
-                    onClick = { surfaceEntity!!.stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE }
+                    onClick = {
+                        surfaceEntity!!.stereoMode =
+                            SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE
+                    }
                 ) {
                     Text(text = "Side-by-Side", fontSize = 10.sp)
                 }
@@ -521,9 +522,11 @@
                 orientedHeight,
                 currentPixelAspectRatio,
             )
-        if (currentSurfaceEntity.canvasShape is SurfaceEntity.CanvasShape.Quad) {
-            currentSurfaceEntity.canvasShape =
-                SurfaceEntity.CanvasShape.Quad(newShapeDimensions.width, newShapeDimensions.height)
+        if (currentSurfaceEntity.shape is SurfaceEntity.Shape.Quad) {
+            currentSurfaceEntity.shape =
+                SurfaceEntity.Shape.Quad(
+                    FloatSize2d(newShapeDimensions.width, newShapeDimensions.height)
+                )
             movableComponent?.size = currentSurfaceEntity.dimensions
         }
 
@@ -566,7 +569,7 @@
             // This is a hack for nonQuad canvas shapes. We don't expect those videos to contain
             // internal rotation, and we want to be able to position the control panel relative to
             // the video panel.
-            if (!(currentSurfaceEntity.canvasShape is SurfaceEntity.CanvasShape.Quad)) {
+            if (!(currentSurfaceEntity.shape is SurfaceEntity.Shape.Quad)) {
                 panel.parent = currentSurfaceEntity
             }
 
@@ -577,7 +580,7 @@
         }
     }
 
-    // Note that pose here will be ignored if the canvasShape is not a Quad
+    // Note that pose here will be ignored if the shape is not a Quad
     // TODO: Update this to take a Pose for the controlPanel
     @Suppress("UnsafeOptInUsageError")
     @Composable
@@ -587,7 +590,7 @@
         videoUri: String,
         stereoMode: Int,
         pose: Pose,
-        canvasShape: SurfaceEntity.CanvasShape,
+        shape: SurfaceEntity.Shape,
         buttonText: String,
         buttonColor: Color = VideoButtonColors.DefaultButton,
         enabled: Boolean = true,
@@ -611,7 +614,7 @@
             enabled = enabled,
             onClick = {
                 var actualPose = pose
-                if (!(canvasShape is SurfaceEntity.CanvasShape.Quad)) {
+                if (!(shape is SurfaceEntity.Shape.Quad)) {
                     actualPose =
                         session.scene.spatialUser.head?.transformPoseTo(
                             Pose.Identity,
@@ -624,9 +627,9 @@
 
                     val surfaceContentLevel =
                         if (protected) {
-                            SurfaceEntity.ContentSecurityLevel.PROTECTED
+                            SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_PROTECTED
                         } else {
-                            SurfaceEntity.ContentSecurityLevel.NONE
+                            SurfaceEntity.SurfaceProtection.SURFACE_PROTECTION_NONE
                         }
 
                     val superSamplingMode =
@@ -634,20 +637,19 @@
                             this@VideoPlayerTestActivity.superSamplingMode ==
                                 SuperSamplingMode.DEFAULT
                         ) {
-                            SurfaceEntity.SuperSampling.DEFAULT
+                            SurfaceEntity.SuperSampling.SUPER_SAMPLING_PENTAGON
                         } else {
-                            SurfaceEntity.SuperSampling.NONE
+                            SurfaceEntity.SuperSampling.SUPER_SAMPLING_NONE
                         }
 
                     surfaceEntity =
                         SurfaceEntity.create(
-                            session,
-                            stereoMode,
-                            actualPose,
-                            canvasShape,
-                            surfaceContentLevel,
-                            null,
-                            superSamplingMode,
+                            session = session,
+                            pose = actualPose,
+                            shape = shape,
+                            stereoMode = stereoMode,
+                            superSampling = superSamplingMode,
+                            surfaceProtection = surfaceContentLevel,
                         )
                     // Make the video player movable (to make it easier to look at it from different
                     // angles and distances) (only on quad canvas)
@@ -655,7 +657,7 @@
                     // The quad has a radius of 1.0 meters
                     movableComponent!!.size = FloatSize3d(1.0f, 1.0f, 1.0f)
 
-                    if (canvasShape is SurfaceEntity.CanvasShape.Quad) {
+                    if (shape is SurfaceEntity.Shape.Quad) {
                         val unused = surfaceEntity!!.addComponent(movableComponent!!)
                     }
                 }
@@ -743,7 +745,7 @@
                                                         colorSpace = colorSpace,
                                                         colorTransfer = colorTransfer,
                                                         colorRange = colorRange,
-                                                        maxCLL = maxContentLightLevel,
+                                                        maxContentLightLevel = maxContentLightLevel,
                                                     )
                                                 surfaceEntity?.contentColorMetadata =
                                                     contentColorMetadata
@@ -829,9 +831,9 @@
             videoUri =
                 Environment.getExternalStorageDirectory().getPath() +
                     "/Download/vid_bigbuckbunny.mp4",
-            stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM,
+            stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM,
             pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
-            canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+            shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
             buttonText = "[Stereo] Play Big Buck Bunny",
             buttonColor = VideoButtonColors.StandardPlayback,
             enabled = enabled,
@@ -854,9 +856,9 @@
             videoUri =
                 Environment.getExternalStorageDirectory().getPath() +
                     "/Download/mvhevc_flat_left_primary_1080.mov",
-            stereoMode = SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+            stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
             pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
-            canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+            shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
             buttonText = "[Multiview] Play MVHEVC Left Primary",
             buttonColor = VideoButtonColors.Multiview,
             enabled = enabled,
@@ -880,9 +882,9 @@
             videoUri =
                 Environment.getExternalStorageDirectory().getPath() +
                     "/Download/mvhevc_flat_right_primary_1080.mov",
-            stereoMode = SurfaceEntity.StereoMode.MULTIVIEW_RIGHT_PRIMARY,
+            stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY,
             pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
-            canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+            shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
             buttonText = "[Multiview] Play MVHEVC Right Primary",
             buttonColor = VideoButtonColors.Multiview,
             enabled = enabled,
@@ -899,9 +901,9 @@
             // For Testers: Note that this translates to "/sdcard/Download/Naver180.mp4".
             videoUri =
                 Environment.getExternalStorageDirectory().getPath() + "/Download/Naver180.mp4",
-            stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE,
+            stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE,
             pose = Pose.Identity, // will be head pose
-            canvasShape = SurfaceEntity.CanvasShape.Vr180Hemisphere(1.0f),
+            shape = SurfaceEntity.Shape.Hemisphere(1.0f),
             buttonText = "[VR] Play Naver 180 (Side-by-Side)",
             buttonColor = VideoButtonColors.VR,
             enabled = enabled,
@@ -918,9 +920,9 @@
             videoUri =
                 Environment.getExternalStorageDirectory().getPath() +
                     "/Download/Galaxy11_VR_3D360.mp4",
-            stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM,
+            stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM,
             pose = Pose.Identity, // will be head pose
-            canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(1.0f),
+            shape = SurfaceEntity.Shape.Sphere(1.0f),
             buttonText = "[VR] Play Galaxy 360 (Top-Bottom)",
             buttonColor = VideoButtonColors.VR,
             enabled = enabled,
@@ -937,9 +939,9 @@
             videoUri =
                 Environment.getExternalStorageDirectory().getPath() +
                     "/Download/Naver180_MV-HEVC.mp4",
-            stereoMode = SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+            stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
             pose = Pose.Identity, // will be head pose
-            canvasShape = SurfaceEntity.CanvasShape.Vr180Hemisphere(1.0f),
+            shape = SurfaceEntity.Shape.Hemisphere(1.0f),
             buttonText = "[VR] Play Naver 180 (MV-HEVC)",
             buttonColor = VideoButtonColors.VR,
             enabled = enabled,
@@ -957,9 +959,9 @@
             videoUri =
                 Environment.getExternalStorageDirectory().getPath() +
                     "/Download/Galaxy11_VR_3D360_MV-HEVC.mp4",
-            stereoMode = SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+            stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
             pose = Pose.Identity, // will be head pose
-            canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(1.0f),
+            shape = SurfaceEntity.Shape.Sphere(1.0f),
             buttonText = "[VR] Play Galaxy 360 (MV-HEVC)",
             buttonColor = VideoButtonColors.VR,
             enabled = enabled,
@@ -982,9 +984,9 @@
             videoUri =
                 Environment.getExternalStorageDirectory().getPath() +
                     "/Download/sdr_singleview_protected.mp4",
-            stereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE,
+            stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE,
             pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
-            canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+            shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
             buttonText = "[DRM] Play Side-by-Side",
             buttonColor = VideoButtonColors.DRM,
             enabled = enabled,
@@ -1008,9 +1010,9 @@
             videoUri =
                 Environment.getExternalStorageDirectory().getPath() +
                     "/Download/mvhevc_flat_left_primary_1080_protected.mp4",
-            stereoMode = SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+            stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
             pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
-            canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+            shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
             buttonText = "[DRM] Play MVHEVC Left Primary",
             buttonColor = VideoButtonColors.DRM,
             enabled = enabled,
@@ -1034,9 +1036,9 @@
             videoUri =
                 Environment.getExternalStorageDirectory().getPath() +
                     "/Download/hdr_pq_1000nits_1080p.mp4",
-            stereoMode = SurfaceEntity.StereoMode.MONO,
+            stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO,
             pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
-            canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+            shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
             buttonText = "[HDR] Play HDR PQ Video",
             buttonColor = VideoButtonColors.HDR,
             enabled = enabled,
@@ -1060,9 +1062,9 @@
             videoUri =
                 Environment.getExternalStorageDirectory().getPath() +
                     "/Download/single_view_rotated_270_half_width.mp4",
-            stereoMode = SurfaceEntity.StereoMode.MONO,
+            stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MONO,
             pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
-            canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+            shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
             buttonText = "[Transform] Play Single View (Rot 270, PAR 0.5)",
             buttonColor = VideoButtonColors.Transformations,
             enabled = enabled,
@@ -1086,9 +1088,9 @@
             videoUri =
                 Environment.getExternalStorageDirectory().getPath() +
                     "/Download/mvhevc_left_primary_rotated_180.mp4",
-            stereoMode = SurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY,
+            stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
             pose = Pose(Vector3(0.0f, 0.0f, -1.5f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f)),
-            canvasShape = SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+            shape = SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
             buttonText = "[Transform] Play MVHEVC Left Primary (Rot 180)",
             buttonColor = VideoButtonColors.Transformations,
             enabled = enabled,
diff --git a/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/ExrImageResourceImpl.java b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/ExrImageResourceImpl.java
new file mode 100644
index 0000000..67ee229
--- /dev/null
+++ b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/ExrImageResourceImpl.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.xr.scenecore.spatial.rendering;
+
+import androidx.xr.runtime.internal.ExrImageResource;
+
+/**
+ * Implementation of a SceneCore ExrImageResource for the Split Engine.
+ *
+ * <p>EXR Images are high dynamic range images that can be used as environmental skyboxes, and can
+ * be used for Image Based Lighting.
+ */
+final class ExrImageResourceImpl implements ExrImageResource {
+    private final long mToken;
+
+    ExrImageResourceImpl(long token) {
+        mToken = token;
+    }
+
+    public long getExtensionImageToken() {
+        return mToken;
+    }
+}
diff --git a/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/GltfModelResourceImpl.java b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/GltfModelResourceImpl.java
new file mode 100644
index 0000000..f4b73de
--- /dev/null
+++ b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/GltfModelResourceImpl.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.xr.scenecore.spatial.rendering;
+
+import androidx.xr.runtime.internal.GltfModelResource;
+
+/**
+ * Implementation of a SceneCore GltfModelResource.
+ *
+ * <p>This is used to create to load a glTF that can later be used when creating a GltfEntity.
+ */
+// TODO: b/417750821 - Add an interface which returns an integer animation IDX given a string
+//                     animation name for a loaded GLTF.
+final class GltfModelResourceImpl implements GltfModelResource {
+    private final long mToken;
+
+    GltfModelResourceImpl(long token) {
+        mToken = token;
+    }
+
+    public long getExtensionModelToken() {
+        return mToken;
+    }
+}
diff --git a/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntime.java b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntime.java
index b95935d..6fefd15 100644
--- a/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntime.java
+++ b/xr/scenecore/scenecore-spatial-rendering/src/main/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntime.java
@@ -21,6 +21,8 @@
 
 import androidx.annotation.VisibleForTesting;
 import androidx.concurrent.futures.ResolvableFuture;
+import androidx.xr.runtime.internal.ExrImageResource;
+import androidx.xr.runtime.internal.GltfModelResource;
 import androidx.xr.runtime.internal.KhronosPbrMaterialSpec;
 import androidx.xr.runtime.internal.MaterialResource;
 import androidx.xr.runtime.internal.RenderingRuntime;
@@ -47,6 +49,8 @@
 import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
+import java.util.function.Supplier;
+
 /**
  * Implementation of [RenderingRuntime] for devices that support the [Feature.SPATIAL] system
  * feature.
@@ -136,6 +140,113 @@
         return new MaterialResourceImpl(token);
     }
 
+    private static GltfModelResourceImpl getModelResourceFromToken(long token) {
+        return new GltfModelResourceImpl(token);
+    }
+
+    private static ExrImageResourceImpl getExrImageResourceFromToken(long token) {
+        return new ExrImageResourceImpl(token);
+    }
+
+    @SuppressWarnings("FutureReturnValueIgnored")
+    private @Nullable ListenableFuture<GltfModelResource> loadGltfAsset(
+            Supplier<ListenableFuture<Long>> modelLoader) {
+        if (!Looper.getMainLooper().isCurrentThread()) {
+            throw new IllegalStateException("This method must be called on the main thread.");
+        }
+
+        ResolvableFuture<GltfModelResource> gltfModelResourceFuture = ResolvableFuture.create();
+
+        ListenableFuture<Long> gltfTokenFuture;
+        try {
+            gltfTokenFuture = modelLoader.get();
+        } catch (RuntimeException e) {
+            return null;
+        }
+
+        gltfTokenFuture.addListener(
+                () -> {
+                    try {
+                        long gltfToken = gltfTokenFuture.get();
+                        gltfModelResourceFuture.set(getModelResourceFromToken(gltfToken));
+                    } catch (Exception e) {
+                        if (e instanceof InterruptedException) {
+                            Thread.currentThread().interrupt();
+                        }
+                        gltfModelResourceFuture.setException(e);
+                    }
+                },
+                mActivity::runOnUiThread);
+
+        return gltfModelResourceFuture;
+    }
+
+    @SuppressWarnings("FutureReturnValueIgnored")
+    private @Nullable ListenableFuture<ExrImageResource> loadExrImage(
+            Supplier<ListenableFuture<Long>> assetLoader) {
+        if (!Looper.getMainLooper().isCurrentThread()) {
+            throw new IllegalStateException("This method must be called on the main thread.");
+        }
+
+        ResolvableFuture<ExrImageResource> exrImageResourceFuture = ResolvableFuture.create();
+
+        ListenableFuture<Long> exrImageTokenFuture;
+        try {
+            exrImageTokenFuture = assetLoader.get();
+        } catch (RuntimeException e) {
+            return null;
+        }
+
+        exrImageTokenFuture.addListener(
+                () -> {
+                    try {
+                        long exrImageToken = exrImageTokenFuture.get();
+                        exrImageResourceFuture.set(getExrImageResourceFromToken(exrImageToken));
+                    } catch (Exception e) {
+                        if (e instanceof InterruptedException) {
+                            Thread.currentThread().interrupt();
+                        }
+                        exrImageResourceFuture.setException(e);
+                    }
+                },
+                mActivity::runOnUiThread);
+
+        return exrImageResourceFuture;
+    }
+
+    // ResolvableFuture is marked as RestrictTo(LIBRARY_GROUP_PREFIX), which is intended for classes
+    // within AndroidX. We're in the process of migrating to AndroidX. Without suppressing this
+    // warning, however, we get a build error - go/bugpattern/RestrictTo.
+    @SuppressWarnings({"RestrictTo", "AsyncSuffixFuture"})
+    @Override
+    public @NonNull ListenableFuture<GltfModelResource> loadGltfByAssetName(@NonNull String name) {
+        return loadGltfAsset(() -> mImpressApi.loadGltfAsset(name));
+    }
+
+    @SuppressWarnings({"RestrictTo", "AsyncSuffixFuture"})
+    @Override
+    public @NonNull ListenableFuture<GltfModelResource> loadGltfByByteArray(
+            byte @NonNull [] assetData, @NonNull String assetKey) {
+        return loadGltfAsset(() -> mImpressApi.loadGltfAsset(assetData, assetKey));
+    }
+
+    // ResolvableFuture is marked as RestrictTo(LIBRARY_GROUP_PREFIX), which is intended for classes
+    // within AndroidX. We're in the process of migrating to AndroidX. Without suppressing this
+    // warning, however, we get a build error - go/bugpattern/RestrictTo.
+    @SuppressWarnings({"RestrictTo", "AsyncSuffixFuture"})
+    @Override
+    public @NonNull ListenableFuture<ExrImageResource> loadExrImageByAssetName(
+            @NonNull String assetName) {
+        return loadExrImage(() -> mImpressApi.loadImageBasedLightingAsset(assetName));
+    }
+
+    @SuppressWarnings({"RestrictTo", "AsyncSuffixFuture"})
+    @Override
+    public @NonNull ListenableFuture<ExrImageResource> loadExrImageByByteArray(
+            byte @NonNull [] assetData, @NonNull String assetKey) {
+        return loadExrImage(() -> mImpressApi.loadImageBasedLightingAsset(assetData, assetKey));
+    }
+
     // ResolvableFuture is marked as RestrictTo(LIBRARY_GROUP_PREFIX), which is intended for classes
     // within AndroidX. We're in the process of migrating to AndroidX. Without suppressing this
     // warning, however, we get a build error - go/bugpattern/RestrictTo.
@@ -193,6 +304,18 @@
         mImpressApi.destroyNativeObject(textureResource.getTextureToken());
     }
 
+    @Override
+    public @Nullable TextureResource getReflectionTextureFromIbl(
+            @NonNull ExrImageResource iblToken) {
+        ExrImageResourceImpl exrImageResource = (ExrImageResourceImpl) iblToken;
+        Texture texture =
+                mImpressApi.getReflectionTextureFromIbl(exrImageResource.getExtensionImageToken());
+        if (texture == null) {
+            return null;
+        }
+        return texture;
+    }
+
     // ResolvableFuture is marked as RestrictTo(LIBRARY_GROUP_PREFIX), which is intended for classes
     // within AndroidX. We're in the process of migrating to AndroidX. Without suppressing this
     // warning, however, we get a build error - go/bugpattern/RestrictTo.
diff --git a/xr/scenecore/scenecore-spatial-rendering/src/test/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntimeTest.java b/xr/scenecore/scenecore-spatial-rendering/src/test/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntimeTest.java
index 7a7047d..3825965 100644
--- a/xr/scenecore/scenecore-spatial-rendering/src/test/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntimeTest.java
+++ b/xr/scenecore/scenecore-spatial-rendering/src/test/java/androidx/xr/scenecore/spatial/rendering/SpatialRenderingRuntimeTest.java
@@ -20,6 +20,8 @@
 
 import android.app.Activity;
 
+import androidx.xr.runtime.internal.ExrImageResource;
+import androidx.xr.runtime.internal.GltfModelResource;
 import androidx.xr.runtime.internal.MaterialResource;
 import androidx.xr.runtime.internal.SceneRuntime;
 import androidx.xr.runtime.internal.SceneRuntimeFactory;
@@ -103,6 +105,66 @@
     }
 
     @Test
+    public void loadGltfByAssetName_returnsModel() throws Exception {
+        ListenableFuture<GltfModelResource> modelFuture =
+                mRuntime.loadGltfByAssetName("FakeAsset.glb");
+
+        assertThat(modelFuture).isNotNull();
+
+        GltfModelResource model = modelFuture.get();
+        assertThat(model).isNotNull();
+        GltfModelResourceImpl modelImpl = (GltfModelResourceImpl) model;
+        assertThat(modelImpl).isNotNull();
+        long token = modelImpl.getExtensionModelToken();
+        assertThat(token).isEqualTo(1);
+    }
+
+    @Test
+    public void loadGltfByByteArray_returnsModel() throws Exception {
+        ListenableFuture<GltfModelResource> modelFuture =
+                mRuntime.loadGltfByByteArray(new byte[] {1, 2, 3}, "FakeAsset.glb");
+
+        assertThat(modelFuture).isNotNull();
+
+        GltfModelResource model = modelFuture.get();
+        assertThat(model).isNotNull();
+        GltfModelResourceImpl modelImpl = (GltfModelResourceImpl) model;
+        assertThat(modelImpl).isNotNull();
+        long token = modelImpl.getExtensionModelToken();
+        assertThat(token).isEqualTo(1);
+    }
+
+    @Test
+    public void loadExrImageByAssetName_returnsModel() throws Exception {
+        ListenableFuture<ExrImageResource> imageFuture =
+                mRuntime.loadExrImageByAssetName("FakeAsset.zip");
+
+        assertThat(imageFuture).isNotNull();
+
+        ExrImageResource image = imageFuture.get();
+        assertThat(image).isNotNull();
+        ExrImageResourceImpl imageImpl = (ExrImageResourceImpl) image;
+        assertThat(imageImpl).isNotNull();
+        long token = imageImpl.getExtensionImageToken();
+        assertThat(token).isEqualTo(1);
+    }
+
+    @Test
+    public void loadExrImageByByteArray_returnsModel() throws Exception {
+        ListenableFuture<ExrImageResource> imageFuture =
+                mRuntime.loadExrImageByByteArray(new byte[] {1, 2, 3}, "FakeAsset.zip");
+
+        assertThat(imageFuture).isNotNull();
+
+        ExrImageResource image = imageFuture.get();
+        assertThat(image).isNotNull();
+        ExrImageResourceImpl imageImpl = (ExrImageResourceImpl) image;
+        assertThat(imageImpl).isNotNull();
+        long token = imageImpl.getExtensionImageToken();
+        assertThat(token).isEqualTo(1);
+    }
+
+    @Test
     public void createWaterMaterial_returnsWaterMaterial() throws Exception {
         assertThat(createWaterMaterial()).isNotNull();
     }
diff --git a/xr/scenecore/scenecore/api/current.txt b/xr/scenecore/scenecore/api/current.txt
index 0a7bc22..d803f9e 100644
--- a/xr/scenecore/scenecore/api/current.txt
+++ b/xr/scenecore/scenecore/api/current.txt
@@ -4,9 +4,9 @@
   public final class ActivityPanelEntity extends androidx.xr.scenecore.PanelEntity {
     method public static androidx.xr.scenecore.ActivityPanelEntity create(androidx.xr.runtime.Session session, androidx.xr.runtime.math.IntSize2d pixelDimensions, String name);
     method public static androidx.xr.scenecore.ActivityPanelEntity create(androidx.xr.runtime.Session session, androidx.xr.runtime.math.IntSize2d pixelDimensions, String name, optional androidx.xr.runtime.math.Pose pose);
-    method public void launchActivity(android.content.Intent intent);
-    method public void launchActivity(android.content.Intent intent, optional android.os.Bundle? bundle);
     method public void moveActivity(android.app.Activity activity);
+    method public void startActivity(android.content.Intent intent);
+    method public void startActivity(android.content.Intent intent, optional android.os.Bundle? bundle);
     field public static final androidx.xr.scenecore.ActivityPanelEntity.Companion Companion;
   }
 
@@ -706,5 +706,104 @@
     field public static final float NO_PREFERRED_ASPECT_RATIO = -1.0f;
   }
 
+  public final class SurfaceEntity extends androidx.xr.scenecore.BaseEntity<androidx.xr.runtime.internal.SurfaceEntity> {
+    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session);
+    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose);
+    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape);
+    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode);
+    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling);
+    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling, optional int surfaceProtection);
+    method public androidx.xr.runtime.math.FloatSize3d getDimensions();
+    method public androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams getEdgeFeatheringParams();
+    method public androidx.xr.scenecore.PerceivedResolutionResult getPerceivedResolution();
+    method public androidx.xr.scenecore.SurfaceEntity.Shape getShape();
+    method public int getStereoMode();
+    method @MainThread public android.view.Surface getSurface();
+    method @MainThread public void setEdgeFeatheringParams(androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams);
+    method @MainThread public void setShape(androidx.xr.scenecore.SurfaceEntity.Shape);
+    method @MainThread public void setStereoMode(int);
+    property public androidx.xr.runtime.math.FloatSize3d dimensions;
+    property public androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams edgeFeatheringParams;
+    property public androidx.xr.scenecore.SurfaceEntity.Shape shape;
+    property public int stereoMode;
+    field public static final androidx.xr.scenecore.SurfaceEntity.Companion Companion;
+  }
+
+  public static final class SurfaceEntity.Companion {
+    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session);
+    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose);
+    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape);
+    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode);
+    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling);
+    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling, optional int surfaceProtection);
+  }
+
+  public abstract static class SurfaceEntity.EdgeFeatheringParams {
+  }
+
+  public static final class SurfaceEntity.EdgeFeatheringParams.NoFeathering extends androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams {
+    ctor public SurfaceEntity.EdgeFeatheringParams.NoFeathering();
+  }
+
+  public static final class SurfaceEntity.EdgeFeatheringParams.RectangleFeather extends androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams {
+    ctor public SurfaceEntity.EdgeFeatheringParams.RectangleFeather();
+    ctor public SurfaceEntity.EdgeFeatheringParams.RectangleFeather(optional @FloatRange(from=0.0, to=0.5) float leftRight, optional @FloatRange(from=0.0, to=0.5) float topBottom);
+    method public float getLeftRight();
+    method public float getTopBottom();
+    property public float leftRight;
+    property public float topBottom;
+  }
+
+  public static interface SurfaceEntity.Shape {
+  }
+
+  public static final class SurfaceEntity.Shape.Hemisphere implements androidx.xr.scenecore.SurfaceEntity.Shape {
+    ctor public SurfaceEntity.Shape.Hemisphere(float radius);
+    method public float getRadius();
+    property public float radius;
+  }
+
+  public static final class SurfaceEntity.Shape.Quad implements androidx.xr.scenecore.SurfaceEntity.Shape {
+    ctor public SurfaceEntity.Shape.Quad(androidx.xr.runtime.math.FloatSize2d extents);
+    method public androidx.xr.runtime.math.FloatSize2d getExtents();
+    property public androidx.xr.runtime.math.FloatSize2d extents;
+  }
+
+  public static final class SurfaceEntity.Shape.Sphere implements androidx.xr.scenecore.SurfaceEntity.Shape {
+    ctor public SurfaceEntity.Shape.Sphere(float radius);
+    method public float getRadius();
+    property public float radius;
+  }
+
+  public static final class SurfaceEntity.StereoMode {
+    property public static int STEREO_MODE_MONO;
+    property public static int STEREO_MODE_MULTIVIEW_LEFT_PRIMARY;
+    property public static int STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY;
+    property public static int STEREO_MODE_SIDE_BY_SIDE;
+    property public static int STEREO_MODE_TOP_BOTTOM;
+    field public static final androidx.xr.scenecore.SurfaceEntity.StereoMode INSTANCE;
+    field public static final int STEREO_MODE_MONO = 0; // 0x0
+    field public static final int STEREO_MODE_MULTIVIEW_LEFT_PRIMARY = 4; // 0x4
+    field public static final int STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY = 5; // 0x5
+    field public static final int STEREO_MODE_SIDE_BY_SIDE = 2; // 0x2
+    field public static final int STEREO_MODE_TOP_BOTTOM = 1; // 0x1
+  }
+
+  public static final class SurfaceEntity.SuperSampling {
+    property public static int SUPER_SAMPLING_NONE;
+    property public static int SUPER_SAMPLING_PENTAGON;
+    field public static final androidx.xr.scenecore.SurfaceEntity.SuperSampling INSTANCE;
+    field public static final int SUPER_SAMPLING_NONE = 0; // 0x0
+    field public static final int SUPER_SAMPLING_PENTAGON = 1; // 0x1
+  }
+
+  public static final class SurfaceEntity.SurfaceProtection {
+    property public static int SURFACE_PROTECTION_NONE;
+    property public static int SURFACE_PROTECTION_PROTECTED;
+    field public static final androidx.xr.scenecore.SurfaceEntity.SurfaceProtection INSTANCE;
+    field public static final int SURFACE_PROTECTION_NONE = 0; // 0x0
+    field public static final int SURFACE_PROTECTION_PROTECTED = 1; // 0x1
+  }
+
 }
 
diff --git a/xr/scenecore/scenecore/api/restricted_current.txt b/xr/scenecore/scenecore/api/restricted_current.txt
index 66eb40c..5792866 100644
--- a/xr/scenecore/scenecore/api/restricted_current.txt
+++ b/xr/scenecore/scenecore/api/restricted_current.txt
@@ -4,9 +4,9 @@
   public final class ActivityPanelEntity extends androidx.xr.scenecore.PanelEntity {
     method public static androidx.xr.scenecore.ActivityPanelEntity create(androidx.xr.runtime.Session session, androidx.xr.runtime.math.IntSize2d pixelDimensions, String name);
     method public static androidx.xr.scenecore.ActivityPanelEntity create(androidx.xr.runtime.Session session, androidx.xr.runtime.math.IntSize2d pixelDimensions, String name, optional androidx.xr.runtime.math.Pose pose);
-    method public void launchActivity(android.content.Intent intent);
-    method public void launchActivity(android.content.Intent intent, optional android.os.Bundle? bundle);
     method public void moveActivity(android.app.Activity activity);
+    method public void startActivity(android.content.Intent intent);
+    method public void startActivity(android.content.Intent intent, optional android.os.Bundle? bundle);
     field public static final androidx.xr.scenecore.ActivityPanelEntity.Companion Companion;
   }
 
@@ -931,182 +931,175 @@
     method public androidx.xr.scenecore.SubspaceNodeEntity create(androidx.xr.runtime.Session session, com.google.androidxr.splitengine.SubspaceNode subspaceNode, androidx.xr.runtime.math.FloatSize3d size);
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class SurfaceEntity extends androidx.xr.scenecore.BaseEntity<androidx.xr.runtime.internal.SurfaceEntity> {
+  public final class SurfaceEntity extends androidx.xr.scenecore.BaseEntity<androidx.xr.runtime.internal.SurfaceEntity> {
     method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session);
-    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode);
-    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose);
-    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape);
-    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape, optional int contentSecurityLevel);
-    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape, optional int contentSecurityLevel, optional androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? contentColorMetadata);
-    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape, optional int contentSecurityLevel, optional androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? contentColorMetadata, optional int superSampling);
-    method public androidx.xr.scenecore.Texture? getAuxiliaryAlphaMaskTexture();
-    method public androidx.xr.scenecore.SurfaceEntity.CanvasShape getCanvasShape();
-    method public androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? getContentColorMetadata();
+    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose);
+    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape);
+    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode);
+    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling);
+    method @MainThread public static androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling, optional int surfaceProtection);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.scenecore.Texture? getAuxiliaryAlphaMaskTexture();
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? getContentColorMetadata();
     method public androidx.xr.runtime.math.FloatSize3d getDimensions();
-    method public androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams getEdgeFeather();
+    method public androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams getEdgeFeatheringParams();
     method public androidx.xr.scenecore.PerceivedResolutionResult getPerceivedResolution();
-    method public androidx.xr.scenecore.Texture? getPrimaryAlphaMaskTexture();
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.scenecore.Texture? getPrimaryAlphaMaskTexture();
+    method public androidx.xr.scenecore.SurfaceEntity.Shape getShape();
     method public int getStereoMode();
     method @MainThread public android.view.Surface getSurface();
-    method @MainThread public void setAuxiliaryAlphaMaskTexture(androidx.xr.scenecore.Texture?);
-    method @MainThread public void setCanvasShape(androidx.xr.scenecore.SurfaceEntity.CanvasShape);
-    method @MainThread public void setContentColorMetadata(androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata?);
-    method @MainThread public void setEdgeFeather(androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams);
-    method @MainThread public void setPrimaryAlphaMaskTexture(androidx.xr.scenecore.Texture?);
+    method @MainThread @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setAuxiliaryAlphaMaskTexture(androidx.xr.scenecore.Texture?);
+    method @MainThread @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setContentColorMetadata(androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata?);
+    method @MainThread public void setEdgeFeatheringParams(androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams);
+    method @MainThread @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public void setPrimaryAlphaMaskTexture(androidx.xr.scenecore.Texture?);
+    method @MainThread public void setShape(androidx.xr.scenecore.SurfaceEntity.Shape);
     method @MainThread public void setStereoMode(int);
-    property public androidx.xr.scenecore.Texture? auxiliaryAlphaMaskTexture;
-    property public androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape;
-    property public androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? contentColorMetadata;
+    property @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.scenecore.Texture? auxiliaryAlphaMaskTexture;
+    property @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? contentColorMetadata;
     property public androidx.xr.runtime.math.FloatSize3d dimensions;
-    property public androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams edgeFeather;
-    property public androidx.xr.scenecore.Texture? primaryAlphaMaskTexture;
+    property public androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams edgeFeatheringParams;
+    property @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.xr.scenecore.Texture? primaryAlphaMaskTexture;
+    property public androidx.xr.scenecore.SurfaceEntity.Shape shape;
     property public int stereoMode;
     field public static final androidx.xr.scenecore.SurfaceEntity.Companion Companion;
   }
 
-  public abstract static class SurfaceEntity.CanvasShape {
-    method public androidx.xr.runtime.math.FloatSize3d getDimensions();
-    property public androidx.xr.runtime.math.FloatSize3d dimensions;
-  }
-
-  public static final class SurfaceEntity.CanvasShape.Quad extends androidx.xr.scenecore.SurfaceEntity.CanvasShape {
-    ctor public SurfaceEntity.CanvasShape.Quad(float width, float height);
-    method public float getHeight();
-    method public float getWidth();
-    property public androidx.xr.runtime.math.FloatSize3d dimensions;
-    property public float height;
-    property public float width;
-  }
-
-  public static final class SurfaceEntity.CanvasShape.Vr180Hemisphere extends androidx.xr.scenecore.SurfaceEntity.CanvasShape {
-    ctor public SurfaceEntity.CanvasShape.Vr180Hemisphere(float radius);
-    method public float getRadius();
-    property public androidx.xr.runtime.math.FloatSize3d dimensions;
-    property public float radius;
-  }
-
-  public static final class SurfaceEntity.CanvasShape.Vr360Sphere extends androidx.xr.scenecore.SurfaceEntity.CanvasShape {
-    ctor public SurfaceEntity.CanvasShape.Vr360Sphere(float radius);
-    method public float getRadius();
-    property public androidx.xr.runtime.math.FloatSize3d dimensions;
-    property public float radius;
-  }
-
   public static final class SurfaceEntity.Companion {
     method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session);
-    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode);
-    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose);
-    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape);
-    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape, optional int contentSecurityLevel);
-    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape, optional int contentSecurityLevel, optional androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? contentColorMetadata);
-    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional int stereoMode, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.CanvasShape canvasShape, optional int contentSecurityLevel, optional androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata? contentColorMetadata, optional int superSampling);
+    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose);
+    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape);
+    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode);
+    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling);
+    method @MainThread public androidx.xr.scenecore.SurfaceEntity create(androidx.xr.runtime.Session session, optional androidx.xr.runtime.math.Pose pose, optional androidx.xr.scenecore.SurfaceEntity.Shape shape, optional int stereoMode, optional int superSampling, optional int surfaceProtection);
   }
 
-  public static final class SurfaceEntity.ContentColorMetadata {
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class SurfaceEntity.ContentColorMetadata {
     ctor public SurfaceEntity.ContentColorMetadata();
-    ctor public SurfaceEntity.ContentColorMetadata(optional int colorSpace, optional int colorTransfer, optional int colorRange, optional int maxCLL);
+    ctor public SurfaceEntity.ContentColorMetadata(optional int colorSpace, optional int colorTransfer, optional int colorRange, optional int maxContentLightLevel);
     method public int getColorRange();
     method public int getColorSpace();
     method public int getColorTransfer();
-    method public int getMaxCLL();
+    method public int getMaxContentLightLevel();
     property public int colorRange;
     property public int colorSpace;
     property public int colorTransfer;
-    property public int maxCLL;
+    property public int maxContentLightLevel;
     field public static final androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata.Companion Companion;
-    field public static final int maxCLLUnknown = 0; // 0x0
+    field public static final int MAX_CONTENT_LIGHT_LEVEL_UNKNOWN = 0; // 0x0
   }
 
   public static final class SurfaceEntity.ContentColorMetadata.ColorRange {
-    property public static int FULL;
-    property public static int LIMITED;
-    field public static final int FULL = 1; // 0x1
+    property public static int COLOR_RANGE_FULL;
+    property public static int COLOR_RANGE_LIMITED;
+    field public static final int COLOR_RANGE_FULL = 1; // 0x1
+    field public static final int COLOR_RANGE_LIMITED = 2; // 0x2
     field public static final androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata.ColorRange INSTANCE;
-    field public static final int LIMITED = 2; // 0x2
   }
 
   public static final class SurfaceEntity.ContentColorMetadata.ColorSpace {
-    property public static int ADOBE_RGB;
-    property public static int BT2020;
-    property public static int BT601_525;
-    property public static int BT601_PAL;
-    property public static int BT709;
-    property public static int DCI_P3;
-    property public static int DISPLAY_P3;
-    field public static final int ADOBE_RGB = 243; // 0xf3
-    field public static final int BT2020 = 6; // 0x6
-    field public static final int BT601_525 = 240; // 0xf0
-    field public static final int BT601_PAL = 2; // 0x2
-    field public static final int BT709 = 1; // 0x1
-    field public static final int DCI_P3 = 242; // 0xf2
-    field public static final int DISPLAY_P3 = 241; // 0xf1
+    property public static int COLOR_SPACE_ADOBE_RGB;
+    property public static int COLOR_SPACE_BT2020;
+    property public static int COLOR_SPACE_BT601_525;
+    property public static int COLOR_SPACE_BT601_PAL;
+    property public static int COLOR_SPACE_BT709;
+    property public static int COLOR_SPACE_DCI_P3;
+    property public static int COLOR_SPACE_DISPLAY_P3;
+    field public static final int COLOR_SPACE_ADOBE_RGB = 243; // 0xf3
+    field public static final int COLOR_SPACE_BT2020 = 6; // 0x6
+    field public static final int COLOR_SPACE_BT601_525 = 240; // 0xf0
+    field public static final int COLOR_SPACE_BT601_PAL = 2; // 0x2
+    field public static final int COLOR_SPACE_BT709 = 1; // 0x1
+    field public static final int COLOR_SPACE_DCI_P3 = 242; // 0xf2
+    field public static final int COLOR_SPACE_DISPLAY_P3 = 241; // 0xf1
     field public static final androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata.ColorSpace INSTANCE;
   }
 
   public static final class SurfaceEntity.ContentColorMetadata.ColorTransfer {
-    property public static int GAMMA_2_2;
-    property public static int HLG;
-    property public static int LINEAR;
-    property public static int SDR;
-    property public static int SRGB;
-    property public static int ST2084;
-    field public static final int GAMMA_2_2 = 10; // 0xa
-    field public static final int HLG = 7; // 0x7
+    property public static int COLOR_TRANSFER_GAMMA_2_2;
+    property public static int COLOR_TRANSFER_HLG;
+    property public static int COLOR_TRANSFER_LINEAR;
+    property public static int COLOR_TRANSFER_SDR;
+    property public static int COLOR_TRANSFER_SRGB;
+    property public static int COLOR_TRANSFER_ST2084;
+    field public static final int COLOR_TRANSFER_GAMMA_2_2 = 10; // 0xa
+    field public static final int COLOR_TRANSFER_HLG = 7; // 0x7
+    field public static final int COLOR_TRANSFER_LINEAR = 1; // 0x1
+    field public static final int COLOR_TRANSFER_SDR = 3; // 0x3
+    field public static final int COLOR_TRANSFER_SRGB = 2; // 0x2
+    field public static final int COLOR_TRANSFER_ST2084 = 6; // 0x6
     field public static final androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata.ColorTransfer INSTANCE;
-    field public static final int LINEAR = 1; // 0x1
-    field public static final int SDR = 3; // 0x3
-    field public static final int SRGB = 2; // 0x2
-    field public static final int ST2084 = 6; // 0x6
   }
 
   public static final class SurfaceEntity.ContentColorMetadata.Companion {
-    property public static int maxCLLUnknown;
-  }
-
-  public static final class SurfaceEntity.ContentSecurityLevel {
-    property public static int NONE;
-    property public static int PROTECTED;
-    field public static final androidx.xr.scenecore.SurfaceEntity.ContentSecurityLevel INSTANCE;
-    field public static final int NONE = 0; // 0x0
-    field public static final int PROTECTED = 1; // 0x1
+    method public androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata getDEFAULT_UNSET_CONTENT_COLOR_METADATA();
+    property public androidx.xr.scenecore.SurfaceEntity.ContentColorMetadata DEFAULT_UNSET_CONTENT_COLOR_METADATA;
+    property public static int MAX_CONTENT_LIGHT_LEVEL_UNKNOWN;
   }
 
   public abstract static class SurfaceEntity.EdgeFeatheringParams {
   }
 
-  public static final class SurfaceEntity.EdgeFeatheringParams.SmoothFeather extends androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams {
-    ctor public SurfaceEntity.EdgeFeatheringParams.SmoothFeather();
-    ctor public SurfaceEntity.EdgeFeatheringParams.SmoothFeather(optional @FloatRange(from=0.0, to=0.5) float leftRight, optional @FloatRange(from=0.0, to=0.5) float topBottom);
+  public static final class SurfaceEntity.EdgeFeatheringParams.NoFeathering extends androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams {
+    ctor public SurfaceEntity.EdgeFeatheringParams.NoFeathering();
+  }
+
+  public static final class SurfaceEntity.EdgeFeatheringParams.RectangleFeather extends androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams {
+    ctor public SurfaceEntity.EdgeFeatheringParams.RectangleFeather();
+    ctor public SurfaceEntity.EdgeFeatheringParams.RectangleFeather(optional @FloatRange(from=0.0, to=0.5) float leftRight, optional @FloatRange(from=0.0, to=0.5) float topBottom);
     method public float getLeftRight();
     method public float getTopBottom();
     property public float leftRight;
     property public float topBottom;
   }
 
-  public static final class SurfaceEntity.EdgeFeatheringParams.SolidEdge extends androidx.xr.scenecore.SurfaceEntity.EdgeFeatheringParams {
-    ctor public SurfaceEntity.EdgeFeatheringParams.SolidEdge();
+  public static interface SurfaceEntity.Shape {
+  }
+
+  public static final class SurfaceEntity.Shape.Hemisphere implements androidx.xr.scenecore.SurfaceEntity.Shape {
+    ctor public SurfaceEntity.Shape.Hemisphere(float radius);
+    method public float getRadius();
+    property public float radius;
+  }
+
+  public static final class SurfaceEntity.Shape.Quad implements androidx.xr.scenecore.SurfaceEntity.Shape {
+    ctor public SurfaceEntity.Shape.Quad(androidx.xr.runtime.math.FloatSize2d extents);
+    method public androidx.xr.runtime.math.FloatSize2d getExtents();
+    property public androidx.xr.runtime.math.FloatSize2d extents;
+  }
+
+  public static final class SurfaceEntity.Shape.Sphere implements androidx.xr.scenecore.SurfaceEntity.Shape {
+    ctor public SurfaceEntity.Shape.Sphere(float radius);
+    method public float getRadius();
+    property public float radius;
   }
 
   public static final class SurfaceEntity.StereoMode {
-    property public static int MONO;
-    property public static int MULTIVIEW_LEFT_PRIMARY;
-    property public static int MULTIVIEW_RIGHT_PRIMARY;
-    property public static int SIDE_BY_SIDE;
-    property public static int TOP_BOTTOM;
+    property public static int STEREO_MODE_MONO;
+    property public static int STEREO_MODE_MULTIVIEW_LEFT_PRIMARY;
+    property public static int STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY;
+    property public static int STEREO_MODE_SIDE_BY_SIDE;
+    property public static int STEREO_MODE_TOP_BOTTOM;
     field public static final androidx.xr.scenecore.SurfaceEntity.StereoMode INSTANCE;
-    field public static final int MONO = 0; // 0x0
-    field public static final int MULTIVIEW_LEFT_PRIMARY = 4; // 0x4
-    field public static final int MULTIVIEW_RIGHT_PRIMARY = 5; // 0x5
-    field public static final int SIDE_BY_SIDE = 2; // 0x2
-    field public static final int TOP_BOTTOM = 1; // 0x1
+    field public static final int STEREO_MODE_MONO = 0; // 0x0
+    field public static final int STEREO_MODE_MULTIVIEW_LEFT_PRIMARY = 4; // 0x4
+    field public static final int STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY = 5; // 0x5
+    field public static final int STEREO_MODE_SIDE_BY_SIDE = 2; // 0x2
+    field public static final int STEREO_MODE_TOP_BOTTOM = 1; // 0x1
   }
 
   public static final class SurfaceEntity.SuperSampling {
-    property public static int DEFAULT;
-    property public static int NONE;
-    field public static final int DEFAULT = 1; // 0x1
+    property public static int SUPER_SAMPLING_NONE;
+    property public static int SUPER_SAMPLING_PENTAGON;
     field public static final androidx.xr.scenecore.SurfaceEntity.SuperSampling INSTANCE;
-    field public static final int NONE = 0; // 0x0
+    field public static final int SUPER_SAMPLING_NONE = 0; // 0x0
+    field public static final int SUPER_SAMPLING_PENTAGON = 1; // 0x1
+  }
+
+  public static final class SurfaceEntity.SurfaceProtection {
+    property public static int SURFACE_PROTECTION_NONE;
+    property public static int SURFACE_PROTECTION_PROTECTED;
+    field public static final androidx.xr.scenecore.SurfaceEntity.SurfaceProtection INSTANCE;
+    field public static final int SURFACE_PROTECTION_NONE = 0; // 0x0
+    field public static final int SURFACE_PROTECTION_PROTECTED = 1; // 0x1
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public class Texture {
@@ -1270,7 +1263,7 @@
     method public androidx.xr.runtime.internal.ResizableComponent createResizableComponent(androidx.xr.runtime.internal.Dimensions, androidx.xr.runtime.internal.Dimensions);
     method public androidx.xr.runtime.internal.SpatialPointerComponent createSpatialPointerComponent();
     method public androidx.xr.runtime.internal.SubspaceNodeEntity createSubspaceNodeEntity(androidx.xr.runtime.SubspaceNodeHolder<? extends java.lang.Object!>, androidx.xr.runtime.internal.Dimensions);
-    method public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(@androidx.xr.runtime.internal.SurfaceEntity.StereoMode int, androidx.xr.runtime.math.Pose, androidx.xr.runtime.internal.SurfaceEntity.CanvasShape, @androidx.xr.runtime.internal.SurfaceEntity.ContentSecurityLevel int, @androidx.xr.runtime.internal.SurfaceEntity.SuperSampling int, androidx.xr.runtime.internal.Entity);
+    method public androidx.xr.runtime.internal.SurfaceEntity createSurfaceEntity(@androidx.xr.runtime.internal.SurfaceEntity.StereoMode int, androidx.xr.runtime.math.Pose, androidx.xr.runtime.internal.SurfaceEntity.Shape, @androidx.xr.runtime.internal.SurfaceEntity.SurfaceProtection int, @androidx.xr.runtime.internal.SurfaceEntity.SuperSampling int, androidx.xr.runtime.internal.Entity);
     method public com.google.common.util.concurrent.ListenableFuture<androidx.xr.runtime.internal.MaterialResource!> createWaterMaterial(boolean);
     method public void destroyKhronosPbrMaterial(androidx.xr.runtime.internal.MaterialResource);
     method public void destroyTexture(androidx.xr.runtime.internal.TextureResource);
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ActivityPanelEntity.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ActivityPanelEntity.kt
index e3f94a1..ccffdd0 100644
--- a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ActivityPanelEntity.kt
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/ActivityPanelEntity.kt
@@ -29,8 +29,9 @@
 /**
  * ActivityPanelEntity creates a spatial panel for embedding an [Activity] in Android XR. Users can
  * either use an [Intent] to launch an Activity in the given panel or provide an instance of
- * Activity to move into this panel. Calling [Entity.dispose] on this Entity will destroy the
- * underlying Activity.
+ * Activity to move into this panel. In order to launch and embed an activity,
+ * [SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY] capability is required. Calling
+ * [Entity.dispose] on this Entity will destroy the underlying Activity.
  */
 public class ActivityPanelEntity
 private constructor(
@@ -40,21 +41,23 @@
 ) : PanelEntity(lifecycleManager, rtActivityPanelEntity, entityManager) {
 
     /**
-     * Launches an [Activity] in the given panel. Subsequent calls to this method will replace the
+     * Starts an [Activity] in the given panel. Subsequent calls to this method will replace the
      * already existing Activity in the panel with the new one. The panel will not be visible until
-     * an Activity is successfully launched. This method will not provide any information about when
-     * the Activity successfully launches.
+     * an Activity is successfully launched. This will fail if the [Scene] does not have the
+     * [SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY] capability. This method will not
+     * provide any information about when the Activity successfully launches.
      *
      * @param intent Intent to launch the activity.
      * @param bundle Bundle to pass to the activity, can be null.
      */
     @JvmOverloads
-    public fun launchActivity(intent: Intent, bundle: Bundle? = null) {
+    public fun startActivity(intent: Intent, bundle: Bundle? = null) {
         rtActivityPanelEntity.launchActivity(intent, bundle)
     }
 
     /**
-     * Moves the given [Activity] into this panel.
+     * Moves the given [Activity] into this panel. This will fail if the application does not have
+     * the [SpatialCapabilities.SPATIAL_CAPABILITY_EMBED_ACTIVITY] capability.
      *
      * @param activity Activity to move into this panel.
      */
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SurfaceEntity.kt b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SurfaceEntity.kt
index 33b13fe..45cd9a6 100644
--- a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SurfaceEntity.kt
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/SurfaceEntity.kt
@@ -16,6 +16,7 @@
 
 package androidx.xr.scenecore
 
+import android.annotation.SuppressLint
 import android.view.Surface
 import androidx.annotation.FloatRange
 import androidx.annotation.IntDef
@@ -26,122 +27,131 @@
 import androidx.xr.runtime.internal.JxrPlatformAdapter
 import androidx.xr.runtime.internal.LifecycleManager
 import androidx.xr.runtime.internal.SurfaceEntity as RtSurfaceEntity
+import androidx.xr.runtime.math.FloatSize2d
 import androidx.xr.runtime.math.FloatSize3d
 import androidx.xr.runtime.math.Pose
 
 /**
- * SurfaceEntity is a concrete implementation of Entity that hosts a StereoSurface Canvas. The
- * entity creates and owns an Android Surface into which the application can render stereo image
- * content. This Surface is then texture mapped to the canvas, and if a stereoscopic StereoMode is
- * specified, then the User will see left and right eye content mapped to the appropriate display.
+ * SurfaceEntity is an [Entity] that hosts a [Surface], which will be texture mapped onto the
+ * [Shape]. If a stereoscopic [StereoMode] is specified, then the User will see left and right eye
+ * content mapped to the appropriate display.
  *
- * Note that it is not currently possible to synchronize CanvasShape and StereoMode changes with
+ * Note that it is not currently possible to synchronize [Shape] and [StereoMode] changes with
  * application rendering or video decoding. Applications are advised to carefully hide this entity
- * around transitions to manage glitchiness.
+ * around state transitions (for example in response to video events) to manage glitchiness.
  *
- * @property canvasShape The [CanvasShape] which describes the mesh to which the Surface is mapped.
+ * @property shape The [Shape] which describes the mesh to which the Surface is mapped.
  * @property stereoMode The [StereoMode] which describes how parts of the surface are displayed to
  *   the user's eyes.
  * @property dimensions The dimensions of the canvas in the local spatial coordinate system of the
  *   entity.
- * @property primaryAlphaMaskTexture The texture to be composited into the alpha channel of the
- *   surface. If null, the alpha mask will be disabled.
- * @property auxiliaryAlphaMaskTexture The texture to be composited into the alpha channel of the
- *   secondary view of the surface. This is only used for interleaved stereo content. If null, the
- *   alpha mask will be disabled.
- * @property edgeFeather The [EdgeFeather] which describes the edge fading effects for the surface.
- * @property contentColorMetadata The [ContentColorMetadata] of the content (nullable).
+ * @property edgeFeatheringParams The [EdgeFeatheringParams] which describes the edge fading effects
+ *   for the surface.
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 public class SurfaceEntity
 private constructor(
     private val lifecycleManager: LifecycleManager,
     rtEntity: RtSurfaceEntity,
     entityManager: EntityManager,
-    canvasShape: CanvasShape,
+    shape: Shape,
     private var disposed: Boolean = false, // TODO b/427314036: remove this
 ) : BaseEntity<RtSurfaceEntity>(rtEntity, entityManager) {
 
     /** Represents the shape of the Canvas that backs a SurfaceEntity. */
-    public abstract class CanvasShape private constructor() {
-        public open val dimensions: FloatSize3d = FloatSize3d(0.0f, 0.0f, 0.0f)
+    public interface Shape {
 
-        // A Quad-shaped canvas. Width and height are represented in the local spatial coordinate
-        // system of the entity. (0,0,0) is the center of the canvas.
-        public class Quad(public val width: Float, public val height: Float) : CanvasShape() {
-            override val dimensions: FloatSize3d
-                get() = FloatSize3d(width, height, 0.0f)
-        }
+        /**
+         * A Quadrilateral-shaped canvas. Width and height are expressed in the X and Y axis in the
+         * local spatial coordinate system of the entity. (0,0) is the center of the Quad mesh; the
+         * lower-left corner of the Quad is at (-width/2, -height/2).
+         *
+         * @property extents The size of the Quad in the local spatial coordinate system of the
+         *   entity.
+         */
+        public class Quad(public val extents: FloatSize2d) : Shape {}
 
-        // An inwards-facing sphere-shaped canvas, centered at (0,0,0) in the local coordinate
-        // space.
-        // This is intended to be used by setting the entity's pose to the user's head pose.
-        // Radius is represented in the local spatial coordinate system of the entity.
-        // The center of the Surface will be mapped to (0, 0, -radius) in the local coordinate
-        // space,
-        // and UV's are applied from positive X to negative X in an equirectangular projection.
-        public class Vr360Sphere(public val radius: Float) : CanvasShape() {
-            override val dimensions: FloatSize3d
-                get() = FloatSize3d(radius * 2, radius * 2, radius * 2)
-        }
+        /**
+         * cal An inwards-facing sphere-shaped mesh, centered at (0,0,0) in the local coordinate
+         * space. This is intended to be used by setting the entity's pose to the user's head pose.
+         * Radius is represented in the local spatial coordinate system of the entity. The center of
+         * the Surface will be mapped to (0, 0, -radius) in the local coordinate space, and texture
+         * coordinate UVs are applied from positive X to negative X in an equirectangular
+         * projection. This means that if this Entity is set to the user's head pose, then they will
+         * be looking at the center of the video if it were viewed in a 2D player.
+         *
+         * @property radius The radius of the sphere in the local spatial coordinate system of the
+         *   entity.
+         */
+        public class Sphere(public val radius: Float) : Shape {}
 
-        // An inwards-facing hemisphere-shaped canvas, where (0,0,0) is the center of the base of
-        // the
-        // hemisphere. Radius is represented in the local spatial coordinate system of the entity.
-        // This is intended to be used by setting the entity's pose to the user's head pose.
-        // The center of the Surface will be mapped to (0, 0, -radius) in the local coordinate
-        // space,
-        // and UV's are applied from positive X to negative X in an equirectangular projection.
-        public class Vr180Hemisphere(public val radius: Float) : CanvasShape() {
-            override val dimensions: FloatSize3d
-                get() = FloatSize3d(radius * 2, radius * 2, radius)
-        }
+        /**
+         * An inwards-facing hemisphere-shaped canvas, where (0,0,0) is the center of the base of
+         * the hemisphere. Radius is represented in the local spatial coordinate system of the
+         * entity. This is intended to be used by setting the entity's pose to the user's head pose.
+         * The center of the Surface will be mapped to (0, 0, -radius) in the local coordinate
+         * space, and texture coordinate UV's are applied from positive X to negative X in an
+         * equirectangular projection.
+         *
+         * @property radius The radius of the hemisphere in the local spatial coordinate system of
+         *   the entity.
+         */
+        public class Hemisphere(public val radius: Float) : Shape {}
     }
 
     /** Represents edge fading effects for a SurfaceEntity. */
     public abstract class EdgeFeatheringParams private constructor() {
         /**
-         * @property leftRight a [Float] which controls the canvas-relative radius of the edge
+         * @property leftRight a [Float] which controls the shape-relative radius of the edge
          *   fadeout on the left and right edges of the SurfaceEntity canvas.
-         * @property topBottom a [Float] which controls the canvas-relative radius of the edge
+         * @property topBottom a [Float] which controls the shape-relative radius of the edge
          *   fadeout on the top and bottom edges of the SurfaceEntity canvas.
          *
          * A radius of 0.05 represents 5% of the width of the visible canvas surface. Please note
          * that this is scaled by the aspect ratio of Quad-shaped canvases.
          *
-         * Applications are encouraged to use ZeroFeather or set this to 0.0 on Spherical canvases.
-         * The behavior is only defined for values between [0.0f - 0.5f]. Default values are 0.0f.
+         * Applications are encouraged to use [NoFeathering] on Spherical canvases. The behavior is
+         * only defined for values between [0.0f - 0.5f]. Default values are 0.0f.
          */
-        public class SmoothFeather(
+        public class RectangleFeather(
             @FloatRange(from = 0.0, to = 0.5) public val leftRight: Float = 0.0f,
             @FloatRange(from = 0.0, to = 0.5) public val topBottom: Float = 0.0f,
         ) : EdgeFeatheringParams() {}
 
         /** Applies no edge fading to any canvas. */
-        public class SolidEdge : EdgeFeatheringParams() {}
+        public class NoFeathering : EdgeFeatheringParams() {}
     }
 
-    @IntDef(ContentSecurityLevel.NONE, ContentSecurityLevel.PROTECTED)
+    @IntDef(
+        SurfaceProtection.SURFACE_PROTECTION_NONE,
+        SurfaceProtection.SURFACE_PROTECTION_PROTECTED,
+    )
     @Retention(AnnotationRetention.SOURCE)
-    internal annotation class ContentSecurityLevelValue
+    internal annotation class SurfaceProtectionValue
 
     /**
-     * Specifies whether the Surface which backs this entity should support DRM content. This is
-     * useful when decoding video content which requires DRM.
+     * Specifies whether the [Surface] which backs this [Entity] should be backed by
+     * [android.hardware.HardwareBuffer]s with the USAGE_PROTECTED_CONTENT flag set. These buffers
+     * support hardware paths for decoding protected content.
      *
-     * See https://developer.android.com/reference/android/media/MediaDrm for more details.
+     * @see https://developer.android.com/reference/android/media/MediaDrm for more details.
      */
-    public object ContentSecurityLevel {
-        // The Surface content is not secured. DRM content can not be decoded into this Surface.
-        // Screen captures of the SurfaceEntity will show the Surface content.
-        public const val NONE: Int = 0
+    public object SurfaceProtection {
+        /**
+         * The Surface content is not protected. Non-protected content can be decoded into this
+         * surface. Protected content can not be decoded into this Surface. Screen captures of the
+         * SurfaceEntity will show the Surface content.
+         */
+        public const val SURFACE_PROTECTION_NONE: Int = 0
 
-        // The surface content is secured. DRM content can be decoded into this Surface.
-        // Screen captures of the SurfaceEntity will redact the Surface content.
-        public const val PROTECTED: Int = 1
+        /**
+         * The Surface content is protected. Non-protected content can be decoded into this surface.
+         * Protected content can be decoded into this Surface. Screen captures of the SurfaceEntity
+         * will redact the Surface content.
+         */
+        public const val SURFACE_PROTECTION_PROTECTED: Int = 1
     }
 
-    @IntDef(SuperSampling.NONE, SuperSampling.DEFAULT)
+    @IntDef(SuperSampling.SUPER_SAMPLING_NONE, SuperSampling.SUPER_SAMPLING_PENTAGON)
     @Retention(AnnotationRetention.SOURCE)
     internal annotation class SuperSamplingValue
 
@@ -150,56 +160,63 @@
      * improve text clarity at a performance cost.
      */
     public object SuperSampling {
-        // Super sampling is disabled.
-        public const val NONE: Int = 0
-        // Super sampling is enabled. This is the default.
-        public const val DEFAULT: Int = 1
+        /** Super sampling is disabled. */
+        public const val SUPER_SAMPLING_NONE: Int = 0
+
+        /**
+         * Super sampling is enabled with a default sampling pattern. This is the value that is set
+         * if SuperSampling is not specified when the Entity is created.
+         */
+        public const val SUPER_SAMPLING_PENTAGON: Int = 1
     }
 
     /**
      * Specifies how the surface content will be routed for stereo viewing. Applications must render
-     * into the surface in accordance with what is specified here in order for the compositor to
+     * into the surface in accordance with what they provided here in order for the compositor to
      * correctly produce a stereoscopic view to the user.
      *
-     * Values here match values from androidx.media3.common.C.StereoMode in
-     * //third_party/java/android_libs/media:common
+     * Values here match values from [androidx.media3.common.C.StereoMode].
+     *
+     * @see https://developer.android.com/reference/androidx/media3/common/C.StereoMode
      */
     @IntDef(
-        StereoMode.MONO,
-        StereoMode.TOP_BOTTOM,
-        StereoMode.SIDE_BY_SIDE,
-        StereoMode.MULTIVIEW_LEFT_PRIMARY,
-        StereoMode.MULTIVIEW_RIGHT_PRIMARY,
+        StereoMode.STEREO_MODE_MONO,
+        StereoMode.STEREO_MODE_TOP_BOTTOM,
+        StereoMode.STEREO_MODE_SIDE_BY_SIDE,
+        StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY,
+        StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY,
     )
     @Retention(AnnotationRetention.SOURCE)
     internal annotation class StereoModeValue
 
     public object StereoMode {
-        // Each eye will see the entire surface (no separation)
-        public const val MONO: Int = 0
-        // The [top, bottom] halves of the surface will map to [left, right] eyes
-        public const val TOP_BOTTOM: Int = 1
-        // The [left, right] halves of the surface will map to [left, right] eyes
-        public const val SIDE_BY_SIDE: Int = 2
-        // Multiview video, [primary, auxiliary] views will map to [left, right] eyes
-        public const val MULTIVIEW_LEFT_PRIMARY: Int = 4
-        // Multiview video, [primary, auxiliary] views will map to [right, left] eyes
-        public const val MULTIVIEW_RIGHT_PRIMARY: Int = 5
+        /** Each eye will see the entire surface (no separation) */
+        public const val STEREO_MODE_MONO: Int = 0
+        /** The [top, bottom] halves of the surface will map to [left, right] eyes */
+        public const val STEREO_MODE_TOP_BOTTOM: Int = 1
+        /** The [left, right] halves of the surface will map to [left, right] eyes */
+        public const val STEREO_MODE_SIDE_BY_SIDE: Int = 2
+        /** Multiview video, [primary, auxiliary] views will map to [left, right] eyes */
+        public const val STEREO_MODE_MULTIVIEW_LEFT_PRIMARY: Int = 4
+        /** Multiview video, [primary, auxiliary] views will map to [right, left] eyes */
+        public const val STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY: Int = 5
     }
 
     /**
-     * Color information for the content drawn on a surface.
+     * Color information for the content drawn on a surface. This is used to hint to the system how
+     * the content should be rendered depending on display settings.
      *
-     * @param colorSpace The color space of the content.
-     * @param colorTransfer The transfer function of the content.
-     * @param colorRange The color range of the content.
-     * @param maxCLL The maximum content light level of the content (in nits).
+     * @property colorSpace The color space of the content.
+     * @property colorTransfer The transfer function to apply to the content.
+     * @property colorRange The color range of the content.
+     * @property maxContentLightLevel The maximum brightness of the content (in nits).
      */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
     public class ContentColorMetadata(
-        @ColorSpaceValue public val colorSpace: Int = ColorSpace.BT709,
-        @ColorTransferValue public val colorTransfer: Int = ColorTransfer.SRGB,
-        @ColorRangeValue public val colorRange: Int = ColorRange.FULL,
-        public val maxCLL: Int = Companion.maxCLLUnknown,
+        @ColorSpaceValue public val colorSpace: Int = ColorSpace.COLOR_SPACE_BT709,
+        @ColorTransferValue public val colorTransfer: Int = ColorTransfer.COLOR_TRANSFER_SRGB,
+        @ColorRangeValue public val colorRange: Int = ColorRange.COLOR_RANGE_FULL,
+        public val maxContentLightLevel: Int = Companion.MAX_CONTENT_LIGHT_LEVEL_UNKNOWN,
     ) {
 
         /**
@@ -208,100 +225,140 @@
          * These values are a superset of androidx.media3.common.C.ColorSpace.
          */
         @IntDef(
-            ColorSpace.BT709,
-            ColorSpace.BT601_PAL,
-            ColorSpace.BT2020,
-            ColorSpace.BT601_525,
-            ColorSpace.DISPLAY_P3,
-            ColorSpace.DCI_P3,
-            ColorSpace.ADOBE_RGB,
+            ColorSpace.COLOR_SPACE_BT709,
+            ColorSpace.COLOR_SPACE_BT601_PAL,
+            ColorSpace.COLOR_SPACE_BT2020,
+            ColorSpace.COLOR_SPACE_BT601_525,
+            ColorSpace.COLOR_SPACE_DISPLAY_P3,
+            ColorSpace.COLOR_SPACE_DCI_P3,
+            ColorSpace.COLOR_SPACE_ADOBE_RGB,
         )
         @Retention(AnnotationRetention.SOURCE)
         internal annotation class ColorSpaceValue
 
         public object ColorSpace {
-            public const val BT709: Int = 1
-            public const val BT601_PAL: Int = 2
-            public const val BT2020: Int = 6
-            public const val BT601_525: Int = 0xf0
-            public const val DISPLAY_P3: Int = 0xf1
-            public const val DCI_P3: Int = 0xf2
-            public const val ADOBE_RGB: Int = 0xf3
+            /** Please see androidx.media3.common.C.COLOR_SPACE_BT709 */
+            public const val COLOR_SPACE_BT709: Int = 1
+            /** Please see androidx.media3.common.C.COLOR_SPACE_BT601 */
+            public const val COLOR_SPACE_BT601_PAL: Int = 2
+            /** Please see androidx.media3.common.C.COLOR_SPACE_BT2020 */
+            public const val COLOR_SPACE_BT2020: Int = 6
+            /** Please see ADataSpace::ADATASPACE_BT601_525 */
+            public const val COLOR_SPACE_BT601_525: Int = 0xf0
+            /** Please see ADataSpace::ADATASPACE_DISPLAY_P3 */
+            public const val COLOR_SPACE_DISPLAY_P3: Int = 0xf1
+            /** Please see ADataSpace::ADATASPACE_DCI_P3 */
+            public const val COLOR_SPACE_DCI_P3: Int = 0xf2
+            /** Please see ADataSpace::ADATASPACE_ADOBE_RGB */
+            public const val COLOR_SPACE_ADOBE_RGB: Int = 0xf3
         }
 
+        @IntDef(
+            ColorTransfer.COLOR_TRANSFER_LINEAR,
+            ColorTransfer.COLOR_TRANSFER_SRGB,
+            ColorTransfer.COLOR_TRANSFER_SDR,
+            ColorTransfer.COLOR_TRANSFER_GAMMA_2_2,
+            ColorTransfer.COLOR_TRANSFER_ST2084,
+            ColorTransfer.COLOR_TRANSFER_HLG,
+        )
+        @Retention(AnnotationRetention.SOURCE)
+        internal annotation class ColorTransferValue
+
         /**
          * Specifies the color transfer function of the media asset drawn on the surface.
          *
          * Enum members cover the transfer functions available in android::ADataSpace Enum values
          * match values from androidx.media3.common.C.ColorTransfer.
          */
-        @IntDef(
-            ColorTransfer.LINEAR,
-            ColorTransfer.SRGB,
-            ColorTransfer.SDR, // SMPTE170M
-            ColorTransfer.GAMMA_2_2,
-            ColorTransfer.ST2084,
-            ColorTransfer.HLG,
-        )
-        @Retention(AnnotationRetention.SOURCE)
-        internal annotation class ColorTransferValue
-
         public object ColorTransfer {
-            public const val LINEAR: Int = 1
-            public const val SRGB: Int = 2
-            public const val SDR: Int = 3
-            public const val GAMMA_2_2: Int = 10
-            public const val ST2084: Int = 6
-            public const val HLG: Int = 7
+            /** Linear transfer characteristic curve. */
+            public const val COLOR_TRANSFER_LINEAR: Int = 1
+            /** The standard RGB transfer function, used for some SDR use-cases like image input. */
+            public const val COLOR_TRANSFER_SRGB: Int = 2
+            /**
+             * SMPTE 170M transfer characteristic curve used by BT.601/BT.709/BT.2020. This is the
+             * curve used by most non-HDR video content.
+             */
+            public const val COLOR_TRANSFER_SDR: Int = 3
+            /** The Gamma 2.2 transfer function, used for some SDR use-cases like tone-mapping. */
+            public const val COLOR_TRANSFER_GAMMA_2_2: Int = 10
+            /** SMPTE ST 2084 transfer function. This is used by some HDR video content. */
+            public const val COLOR_TRANSFER_ST2084: Int = 6
+            /**
+             * ARIB STD-B67 hybrid-log-gamma transfer function. This is used by some HDR video
+             * content.
+             */
+            public const val COLOR_TRANSFER_HLG: Int = 7
         }
 
+        @IntDef(ColorRange.COLOR_RANGE_FULL, ColorRange.COLOR_RANGE_LIMITED)
+        @Retention(AnnotationRetention.SOURCE)
+        internal annotation class ColorRangeValue
+
         /**
          * Specifies the color range of the media asset drawn on the surface.
          *
          * Enum values match values from androidx.media3.common.C.ColorRange.
          */
-        @IntDef(ColorRange.FULL, ColorRange.LIMITED)
-        @Retention(AnnotationRetention.SOURCE)
-        internal annotation class ColorRangeValue
-
         public object ColorRange {
-            public const val FULL: Int = 1
-            public const val LIMITED: Int = 2
+            /** Please see android.media.MediaFormat.COLOR_RANGE_FULL */
+            public const val COLOR_RANGE_FULL: Int = 1
+            /** Please see android.media.MedaiFormat.COLOR_RANGE_LIMITED */
+            public const val COLOR_RANGE_LIMITED: Int = 2
         }
 
         public companion object {
-            // Represents an unknown maximum content light level.
-            public const val maxCLLUnknown: Int = 0
+            /**
+             * Represents an unknown maximum content light level.
+             *
+             * Note that the smallest value for this is 1 nit, so this value should only be used if
+             * the actual value is unknown or if the content is constant luminance.
+             */
+            public const val MAX_CONTENT_LIGHT_LEVEL_UNKNOWN: Int = 0
+
+            /**
+             * A Default (unset) value for ContentColorMetadata. Setting this will cause the system
+             * to render the content according to values set on the underlying [HardwareBuffer]s;
+             * these are usually set correctly by the MediaCodec.
+             */
+            public val DEFAULT_UNSET_CONTENT_COLOR_METADATA: ContentColorMetadata =
+                ContentColorMetadata(
+                    colorSpace = ColorSpace.COLOR_SPACE_BT709,
+                    colorTransfer = ColorTransfer.COLOR_TRANSFER_SRGB,
+                    colorRange = ColorRange.COLOR_RANGE_FULL,
+                    maxContentLightLevel = MAX_CONTENT_LIGHT_LEVEL_UNKNOWN,
+                )
 
             internal fun getRtColorSpace(colorSpace: Int): Int {
                 return when (colorSpace) {
-                    ColorSpace.BT709 -> RtSurfaceEntity.ColorSpace.BT709
-                    ColorSpace.BT601_PAL -> RtSurfaceEntity.ColorSpace.BT601_PAL
-                    ColorSpace.BT2020 -> RtSurfaceEntity.ColorSpace.BT2020
-                    ColorSpace.BT601_525 -> RtSurfaceEntity.ColorSpace.BT601_525
-                    ColorSpace.DISPLAY_P3 -> RtSurfaceEntity.ColorSpace.DISPLAY_P3
-                    ColorSpace.DCI_P3 -> RtSurfaceEntity.ColorSpace.DCI_P3
-                    ColorSpace.ADOBE_RGB -> RtSurfaceEntity.ColorSpace.ADOBE_RGB
+                    ColorSpace.COLOR_SPACE_BT709 -> RtSurfaceEntity.ColorSpace.BT709
+                    ColorSpace.COLOR_SPACE_BT601_PAL -> RtSurfaceEntity.ColorSpace.BT601_PAL
+                    ColorSpace.COLOR_SPACE_BT2020 -> RtSurfaceEntity.ColorSpace.BT2020
+                    ColorSpace.COLOR_SPACE_BT601_525 -> RtSurfaceEntity.ColorSpace.BT601_525
+                    ColorSpace.COLOR_SPACE_DISPLAY_P3 -> RtSurfaceEntity.ColorSpace.DISPLAY_P3
+                    ColorSpace.COLOR_SPACE_DCI_P3 -> RtSurfaceEntity.ColorSpace.DCI_P3
+                    ColorSpace.COLOR_SPACE_ADOBE_RGB -> RtSurfaceEntity.ColorSpace.ADOBE_RGB
                     else -> RtSurfaceEntity.ColorSpace.BT709
                 }
             }
 
             internal fun getRtColorTransfer(colorTransfer: Int): Int {
                 return when (colorTransfer) {
-                    ColorTransfer.LINEAR -> RtSurfaceEntity.ColorTransfer.LINEAR
-                    ColorTransfer.SRGB -> RtSurfaceEntity.ColorTransfer.SRGB
-                    ColorTransfer.SDR -> RtSurfaceEntity.ColorTransfer.SDR
-                    ColorTransfer.GAMMA_2_2 -> RtSurfaceEntity.ColorTransfer.GAMMA_2_2
-                    ColorTransfer.ST2084 -> RtSurfaceEntity.ColorTransfer.ST2084
-                    ColorTransfer.HLG -> RtSurfaceEntity.ColorTransfer.HLG
+                    ColorTransfer.COLOR_TRANSFER_LINEAR -> RtSurfaceEntity.ColorTransfer.LINEAR
+                    ColorTransfer.COLOR_TRANSFER_SRGB -> RtSurfaceEntity.ColorTransfer.SRGB
+                    ColorTransfer.COLOR_TRANSFER_SDR -> RtSurfaceEntity.ColorTransfer.SDR
+                    ColorTransfer.COLOR_TRANSFER_GAMMA_2_2 ->
+                        RtSurfaceEntity.ColorTransfer.GAMMA_2_2
+                    ColorTransfer.COLOR_TRANSFER_ST2084 -> RtSurfaceEntity.ColorTransfer.ST2084
+                    ColorTransfer.COLOR_TRANSFER_HLG -> RtSurfaceEntity.ColorTransfer.HLG
                     else -> RtSurfaceEntity.ColorTransfer.SRGB
                 }
             }
 
             internal fun getRtColorRange(colorRange: Int): Int {
                 return when (colorRange) {
-                    ColorRange.FULL -> RtSurfaceEntity.ColorRange.FULL
-                    ColorRange.LIMITED -> RtSurfaceEntity.ColorRange.LIMITED
+                    ColorRange.COLOR_RANGE_FULL -> RtSurfaceEntity.ColorRange.FULL
+                    ColorRange.COLOR_RANGE_LIMITED -> RtSurfaceEntity.ColorRange.LIMITED
                     else -> RtSurfaceEntity.ColorRange.FULL
                 }
             }
@@ -324,28 +381,29 @@
     public companion object {
         private fun getRtStereoMode(stereoMode: Int): Int {
             return when (stereoMode) {
-                StereoMode.MONO -> RtSurfaceEntity.StereoMode.MONO
-                StereoMode.TOP_BOTTOM -> RtSurfaceEntity.StereoMode.TOP_BOTTOM
-                StereoMode.MULTIVIEW_LEFT_PRIMARY ->
+                StereoMode.STEREO_MODE_MONO -> RtSurfaceEntity.StereoMode.MONO
+                StereoMode.STEREO_MODE_TOP_BOTTOM -> RtSurfaceEntity.StereoMode.TOP_BOTTOM
+                StereoMode.STEREO_MODE_MULTIVIEW_LEFT_PRIMARY ->
                     RtSurfaceEntity.StereoMode.MULTIVIEW_LEFT_PRIMARY
-                StereoMode.MULTIVIEW_RIGHT_PRIMARY ->
+                StereoMode.STEREO_MODE_MULTIVIEW_RIGHT_PRIMARY ->
                     RtSurfaceEntity.StereoMode.MULTIVIEW_RIGHT_PRIMARY
                 else -> RtSurfaceEntity.StereoMode.SIDE_BY_SIDE
             }
         }
 
-        private fun getRtContentSecurityLevel(contentSecurityLevel: Int): Int {
-            return when (contentSecurityLevel) {
-                ContentSecurityLevel.NONE -> RtSurfaceEntity.ContentSecurityLevel.NONE
-                ContentSecurityLevel.PROTECTED -> RtSurfaceEntity.ContentSecurityLevel.PROTECTED
-                else -> RtSurfaceEntity.ContentSecurityLevel.NONE
+        private fun getRtSurfaceProtection(surfaceProtection: Int): Int {
+            return when (surfaceProtection) {
+                SurfaceProtection.SURFACE_PROTECTION_NONE -> RtSurfaceEntity.SurfaceProtection.NONE
+                SurfaceProtection.SURFACE_PROTECTION_PROTECTED ->
+                    RtSurfaceEntity.SurfaceProtection.PROTECTED
+                else -> RtSurfaceEntity.SurfaceProtection.NONE
             }
         }
 
         private fun getRtSuperSampling(superSampling: Int): Int {
             return when (superSampling) {
-                SuperSampling.NONE -> RtSurfaceEntity.SuperSampling.NONE
-                SuperSampling.DEFAULT -> RtSurfaceEntity.SuperSampling.DEFAULT
+                SuperSampling.SUPER_SAMPLING_NONE -> RtSurfaceEntity.SuperSampling.NONE
+                SuperSampling.SUPER_SAMPLING_PENTAGON -> RtSurfaceEntity.SuperSampling.DEFAULT
                 else -> RtSurfaceEntity.SuperSampling.DEFAULT
             }
         }
@@ -358,9 +416,10 @@
          * @param entityManager A SceneCore EntityManager
          * @param stereoMode An [Int] which defines how surface subregions map to eyes
          * @param pose Pose for this StereoSurface entity, relative to its parent.
-         * @param canvasShape The [CanvasShape] which describes the spatialized shape of the canvas.
-         * @param contentSecurityLevel The [ContentSecurityLevel] which describes whether DRM is
-         *   enabled for the surface.
+         * @param shape The [Shape] which describes the spatialized shape of the canvas.
+         * @param surfaceProtection The Int member of [SurfaceProtection] which describes whether
+         *   DRM is enabled for the surface - which will create protected hardware buffers for
+         *   presentation.
          * @param contentColorMetadata The [ContentColorMetadata] of the content (nullable).
          * @param superSampling The [SuperSampling] which describes whether super sampling is
          *   enabled for the surface.
@@ -370,22 +429,20 @@
             lifecycleManager: LifecycleManager,
             adapter: JxrPlatformAdapter,
             entityManager: EntityManager,
-            stereoMode: Int = StereoMode.SIDE_BY_SIDE,
+            @StereoModeValue stereoMode: Int = StereoMode.STEREO_MODE_MONO,
             pose: Pose = Pose.Identity,
-            canvasShape: CanvasShape = CanvasShape.Quad(1.0f, 1.0f),
-            contentSecurityLevel: Int = ContentSecurityLevel.NONE,
+            shape: Shape = Shape.Quad(FloatSize2d(1.0f, 1.0f)),
+            @SurfaceProtectionValue
+            surfaceProtection: Int = SurfaceProtection.SURFACE_PROTECTION_NONE,
             contentColorMetadata: ContentColorMetadata? = null,
-            superSampling: Int = SuperSampling.DEFAULT,
+            @SuperSamplingValue superSampling: Int = SuperSampling.SUPER_SAMPLING_PENTAGON,
         ): SurfaceEntity {
-            val rtCanvasShape =
-                when (canvasShape) {
-                    is CanvasShape.Quad ->
-                        RtSurfaceEntity.CanvasShape.Quad(canvasShape.width, canvasShape.height)
-                    is CanvasShape.Vr360Sphere ->
-                        RtSurfaceEntity.CanvasShape.Vr360Sphere(canvasShape.radius)
-                    is CanvasShape.Vr180Hemisphere ->
-                        RtSurfaceEntity.CanvasShape.Vr180Hemisphere(canvasShape.radius)
-                    else -> throw IllegalArgumentException("Unsupported canvas shape: $canvasShape")
+            val rtShape =
+                when (shape) {
+                    is Shape.Quad -> RtSurfaceEntity.Shape.Quad(shape.extents)
+                    is Shape.Sphere -> RtSurfaceEntity.Shape.Sphere(shape.radius)
+                    is Shape.Hemisphere -> RtSurfaceEntity.Shape.Hemisphere(shape.radius)
+                    else -> throw IllegalArgumentException("Unsupported shape: $shape")
                 }
             val surfaceEntity =
                 SurfaceEntity(
@@ -393,13 +450,13 @@
                     adapter.createSurfaceEntity(
                         getRtStereoMode(stereoMode),
                         pose,
-                        rtCanvasShape,
-                        getRtContentSecurityLevel(contentSecurityLevel),
+                        rtShape,
+                        getRtSurfaceProtection(surfaceProtection),
                         getRtSuperSampling(superSampling),
                         adapter.activitySpaceRootImpl,
                     ),
                     entityManager,
-                    canvasShape,
+                    shape,
                 )
             surfaceEntity.contentColorMetadata = contentColorMetadata
             return surfaceEntity
@@ -408,16 +465,12 @@
         /**
          * Public factory function for a SurfaceEntity.
          *
-         * This method must be called from the main thread.
-         * https://developer.android.com/guide/components/processes-and-threads
-         *
          * @param session Session to create the SurfaceEntity in.
          * @param stereoMode Stereo mode for the surface.
          * @param pose Pose of this entity relative to its parent, default value is Identity.
-         * @param canvasShape The [CanvasShape] which describes the spatialized shape of the canvas.
-         * @param contentSecurityLevel The [ContentSecurityLevel] which describes whether DRM is
-         *   enabled for the surface.
-         * @param contentColorMetadata The [ContentColorMetadata] of the content (nullable).
+         * @param shape The [Shape] which describes the spatialized shape of the canvas.
+         * @param surfaceProtection The [SurfaceProtection] which describes whether the hosted
+         *   surface should support Widevine DRM.
          * @param superSampling The [SuperSampling] which describes whether super sampling is
          *   enabled for the surface.
          * @return a SurfaceEntity instance
@@ -427,12 +480,12 @@
         @JvmStatic
         public fun create(
             session: Session,
-            stereoMode: Int = SurfaceEntity.StereoMode.SIDE_BY_SIDE,
             pose: Pose = Pose.Identity,
-            canvasShape: CanvasShape = CanvasShape.Quad(1.0f, 1.0f),
-            contentSecurityLevel: Int = ContentSecurityLevel.NONE,
-            contentColorMetadata: ContentColorMetadata? = null,
-            superSampling: Int = SuperSampling.DEFAULT,
+            shape: Shape = Shape.Quad(FloatSize2d(1.0f, 1.0f)),
+            @StereoModeValue stereoMode: Int = StereoMode.STEREO_MODE_MONO,
+            @SuperSamplingValue superSampling: Int = SuperSampling.SUPER_SAMPLING_PENTAGON,
+            @SurfaceProtectionValue
+            surfaceProtection: Int = SurfaceProtection.SURFACE_PROTECTION_NONE,
         ): SurfaceEntity =
             SurfaceEntity.create(
                 session.runtime.lifecycleManager,
@@ -440,9 +493,9 @@
                 session.scene.entityManager,
                 stereoMode,
                 pose,
-                canvasShape,
-                contentSecurityLevel,
-                contentColorMetadata,
+                shape,
+                surfaceProtection,
+                null,
                 superSampling,
             )
     }
@@ -452,10 +505,9 @@
      * into the surface in accordance with what is specified here in order for the compositor to
      * correctly produce a stereoscopic view to the user.
      *
-     * Values must be one of the values from [StereoMode].
-     *
      * @throws IllegalStateException when setting this value if the Entity has been disposed.
      */
+    @StereoModeValue
     public var stereoMode: Int
         get() {
             checkDisposed()
@@ -468,11 +520,9 @@
         }
 
     /**
-     * Returns the dimensions of the Entity.
+     * Returns the size of the canvas in the local spatial coordinate system of the entity.
      *
-     * This is the size of the canvas in the local spatial coordinate system of the entity. This
-     * field cannot be directly set - to update the dimensions of the canvas, update the value of
-     * [canvasShape].
+     * This value is entirely determined by the value of [shape].
      */
     public val dimensions: FloatSize3d
         get() {
@@ -481,27 +531,24 @@
         }
 
     /**
-     * The shape of the canvas that backs the Entity. Updating this value will alter the dimensions
-     * of the Entity.
+     * The shape of the canvas that backs the Entity. Updating this value will alter the
+     * [dimensions] of the Entity.
      *
      * @throws IllegalArgumentException if an invalid canvas shape is provided.
      * @throws IllegalStateException when setting this value if the Entity has been disposed.
      */
-    public var canvasShape: CanvasShape = canvasShape
+    public var shape: Shape = shape
         @MainThread
         set(value) {
             checkDisposed()
-            val rtCanvasShape =
+            val rtShape =
                 when (value) {
-                    is CanvasShape.Quad ->
-                        RtSurfaceEntity.CanvasShape.Quad(value.width, value.height)
-                    is CanvasShape.Vr360Sphere ->
-                        RtSurfaceEntity.CanvasShape.Vr360Sphere(value.radius)
-                    is CanvasShape.Vr180Hemisphere ->
-                        RtSurfaceEntity.CanvasShape.Vr180Hemisphere(value.radius)
+                    is Shape.Quad -> RtSurfaceEntity.Shape.Quad(value.extents)
+                    is Shape.Sphere -> RtSurfaceEntity.Shape.Sphere(value.radius)
+                    is Shape.Hemisphere -> RtSurfaceEntity.Shape.Hemisphere(value.radius)
                     else -> throw IllegalArgumentException("Unsupported canvas shape: $value")
                 }
-            rtEntity.canvasShape = rtCanvasShape
+            rtEntity.shape = rtShape
             field = value
         }
 
@@ -511,8 +558,16 @@
      *
      * @throws IllegalStateException when setting this value if the Entity has been disposed.
      */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
     public var primaryAlphaMaskTexture: Texture? = null
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+        get() {
+            checkDisposed()
+            return field
+        }
         @MainThread
+        @SuppressLint("HiddenTypeParameter")
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
         set(value) {
             checkDisposed()
             rtEntity.setPrimaryAlphaMaskTexture(value?.texture)
@@ -525,8 +580,16 @@
      *
      * @throws IllegalStateException when setting this value if the Entity has been disposed.
      */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
     public var auxiliaryAlphaMaskTexture: Texture? = null
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+        get() {
+            checkDisposed()
+            return field
+        }
         @MainThread
+        @SuppressLint("HiddenTypeParameter")
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
         set(value) {
             checkDisposed()
             rtEntity.setAuxiliaryAlphaMaskTexture(value?.texture)
@@ -534,12 +597,12 @@
         }
 
     /**
-     * The [EdgeFeather] feathering pattern to be used along the edges of the CanvasShape. This
+     * The [EdgeFeatheringParams] feathering pattern to be used along the edges of the Shape. This
      * value must only be set from the main thread.
      *
      * @throws IllegalStateException when setting this value if the Entity has been disposed.
      */
-    public var edgeFeather: EdgeFeatheringParams = EdgeFeatheringParams.SolidEdge()
+    public var edgeFeatheringParams: EdgeFeatheringParams = EdgeFeatheringParams.NoFeathering()
         get() {
             checkDisposed()
             return field
@@ -549,9 +612,13 @@
             checkDisposed()
             val rtEdgeFeather =
                 when (value) {
-                    is EdgeFeatheringParams.SolidEdge -> RtSurfaceEntity.EdgeFeather.SolidEdge()
-                    is EdgeFeatheringParams.SmoothFeather ->
-                        RtSurfaceEntity.EdgeFeather.SmoothFeather(value.leftRight, value.topBottom)
+                    is EdgeFeatheringParams.NoFeathering ->
+                        RtSurfaceEntity.EdgeFeather.NoFeathering()
+                    is EdgeFeatheringParams.RectangleFeather ->
+                        RtSurfaceEntity.EdgeFeather.RectangleFeather(
+                            value.leftRight,
+                            value.topBottom,
+                        )
                     else -> throw IllegalArgumentException("Unsupported edge feather: $value")
                 }
             rtEntity.edgeFeather = rtEdgeFeather
@@ -561,29 +628,31 @@
     /**
      * Manages the explicit [ContentColorMetadata] for the surface's content.
      *
-     * Describes how the application wants the system renderer to color convert Surface content to
+     * Describes how the application wants the system renderer to color convert [Surface] content to
      * the Display. When this is null, the system will make a best guess at the appropriate
-     * conversion.
+     * conversion. Most applications will not need to set this - video playback libraries such as
+     * ExoPlayer will automatically apply the correct conversion for media playback. Applications
+     * rendering to the surface using APIs such as Vulkan are encouraged to use Vulkan extensions to
+     * specify the color space and transfer function of their content and leave this value as null.
      *
      * The setter must be called from the main thread.
      *
      * @throws IllegalStateException when setting this value if the Entity has been disposed.
      */
-    public var contentColorMetadata: ContentColorMetadata?
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    public var contentColorMetadata: ContentColorMetadata? = null
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
         get() {
             checkDisposed()
             return if (!rtEntity.contentColorMetadataSet) {
                 null
             } else {
-                ContentColorMetadata(
-                    colorSpace = rtEntity.colorSpace,
-                    colorTransfer = rtEntity.colorTransfer,
-                    colorRange = rtEntity.colorRange,
-                    maxCLL = rtEntity.maxCLL,
-                )
+                return field
             }
         }
         @MainThread
+        @SuppressLint("HiddenTypeParameter")
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
         set(value) {
             checkDisposed()
             if (value == null) {
@@ -593,18 +662,16 @@
                     ContentColorMetadata.getRtColorSpace(value.colorSpace),
                     ContentColorMetadata.getRtColorTransfer(value.colorTransfer),
                     ContentColorMetadata.getRtColorRange(value.colorRange),
-                    value.maxCLL,
+                    value.maxContentLightLevel,
                 )
             }
+            field = value
         }
 
     /**
      * Returns a surface into which the application can render stereo image content. Note that
      * android.graphics.Canvas Apis are not currently supported on this Canvas.
      *
-     * This method must be called from the main thread.
-     * https://developer.android.com/guide/components/processes-and-threads
-     *
      * @throws IllegalStateException if the Entity has been disposed.
      */
     @MainThread
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxr.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxr.java
index 2715ca7..9f8cc7e 100644
--- a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxr.java
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxr.java
@@ -1540,8 +1540,8 @@
     public @NonNull SurfaceEntity createSurfaceEntity(
             @SurfaceEntity.StereoMode int stereoMode,
             @NonNull Pose pose,
-            SurfaceEntity.@NonNull CanvasShape canvasShape,
-            @SurfaceEntity.ContentSecurityLevel int contentSecurityLevel,
+            SurfaceEntity.@NonNull Shape shape,
+            @SurfaceEntity.SurfaceProtection int surfaceProtection,
             @SurfaceEntity.SuperSampling int superSampling,
             @NonNull Entity parentEntity) {
         if (!mUseSplitEngine) {
@@ -1550,8 +1550,8 @@
         } else {
             return createSurfaceEntitySplitEngine(
                     stereoMode,
-                    canvasShape,
-                    contentSecurityLevel,
+                    shape,
+                    surfaceProtection,
                     superSampling,
                     pose,
                     parentEntity);
@@ -1909,8 +1909,8 @@
 
     private SurfaceEntity createSurfaceEntitySplitEngine(
             @SurfaceEntity.StereoMode int stereoMode,
-            SurfaceEntity.CanvasShape canvasShape,
-            @SurfaceEntity.ContentSecurityLevel int contentSecurityLevel,
+            SurfaceEntity.Shape shape,
+            @SurfaceEntity.SurfaceProtection int surfaceProtection,
             @SurfaceEntity.SuperSampling int superSampling,
             Pose pose,
             @NonNull Entity parentEntity) {
@@ -1929,8 +1929,8 @@
                         mEntityManager,
                         mExecutor,
                         stereoMode,
-                        canvasShape,
-                        contentSecurityLevel,
+                        shape,
+                        surfaceProtection,
                         superSampling);
         entity.setPose(pose, Space.PARENT);
         return entity;
diff --git a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SurfaceEntityImpl.java b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SurfaceEntityImpl.java
index a33acd3..3ece972 100644
--- a/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SurfaceEntityImpl.java
+++ b/xr/scenecore/scenecore/src/main/java/androidx/xr/scenecore/impl/SurfaceEntityImpl.java
@@ -26,7 +26,7 @@
 import androidx.xr.runtime.internal.PerceivedResolutionResult;
 import androidx.xr.runtime.internal.Space;
 import androidx.xr.runtime.internal.SurfaceEntity;
-import androidx.xr.runtime.internal.SurfaceEntity.CanvasShape;
+import androidx.xr.runtime.internal.SurfaceEntity.Shape;
 import androidx.xr.runtime.internal.TextureResource;
 import androidx.xr.runtime.math.Vector3;
 import androidx.xr.scenecore.impl.impress.ImpressApi;
@@ -57,12 +57,12 @@
     private final int mSubspaceImpressNode;
     @StereoMode private int mStereoMode = SurfaceEntity.StereoMode.SIDE_BY_SIDE;
 
-    @ContentSecurityLevel
-    private int mContentSecurityLevel = SurfaceEntity.ContentSecurityLevel.NONE;
+    @SurfaceProtection
+    private int mSurfaceProtection = SurfaceEntity.SurfaceProtection.NONE;
 
     @SuperSampling private int mSuperSampling = SurfaceEntity.SuperSampling.DEFAULT;
 
-    private CanvasShape mCanvasShape;
+    private Shape mShape;
     private EdgeFeather mEdgeFeather;
     private boolean mContentColorMetadataSet = false;
     @ColorSpace private int mColorSpace = SurfaceEntity.ColorSpace.BT709;
@@ -73,19 +73,19 @@
     // still allowing the application to hold a reference to the SurfaceEntity.
     private SubspaceNode mSubspace = null;
 
-    // Converts SurfaceEntity's ContentSecurityLevel to an Impress ContentSecurityLevel.
+    // Converts SurfaceEntity's SurfaceProtection to an Impress ContentSecurityLevel.
     private static int toImpressContentSecurityLevel(
-            @ContentSecurityLevel int contentSecurityLevel) {
-        switch (contentSecurityLevel) {
-            case ContentSecurityLevel.NONE:
+            @SurfaceProtection int surfaceProtection) {
+        switch (surfaceProtection) {
+            case SurfaceProtection.NONE:
                 return ImpressApi.ContentSecurityLevel.NONE;
-            case ContentSecurityLevel.PROTECTED:
+            case SurfaceProtection.PROTECTED:
                 return ImpressApi.ContentSecurityLevel.PROTECTED;
             default:
                 Log.e(
                         "SurfaceEntityImpl",
-                        "Unsupported content security level: "
-                                + contentSecurityLevel
+                        "Unsupported surface protection level: "
+                                + surfaceProtection
                                 + ". Defaulting to NONE.");
                 return ImpressApi.ContentSecurityLevel.NONE;
         }
@@ -117,16 +117,16 @@
             EntityManager entityManager,
             ScheduledExecutorService executor,
             @StereoMode int stereoMode,
-            CanvasShape canvasShape,
-            @ContentSecurityLevel int contentSecurityLevel,
+            Shape shape,
+            @SurfaceProtection int surfaceProtection,
             @SuperSampling int superSampling) {
         super(context, extensions.createNode(), extensions, entityManager, executor);
         mImpressApi = impressApi;
         mSplitEngineSubspaceManager = splitEngineSubspaceManager;
         mStereoMode = stereoMode;
-        mContentSecurityLevel = contentSecurityLevel;
+        mSurfaceProtection = surfaceProtection;
         mSuperSampling = superSampling;
-        mCanvasShape = canvasShape;
+        mShape = shape;
         setParent(parentEntity);
 
         // System will only render Impress nodes that are parented by this subspace node.
@@ -145,12 +145,12 @@
             mEntityImpressNode =
                     mImpressApi.createStereoSurface(
                             stereoMode,
-                            toImpressContentSecurityLevel(mContentSecurityLevel),
+                            toImpressContentSecurityLevel(mSurfaceProtection),
                             toImpressSuperSampling(mSuperSampling));
         } catch (IllegalArgumentException e) {
             throw new IllegalStateException(e);
         }
-        setCanvasShape(mCanvasShape);
+        setShape(mShape);
 
         // The CPM node hierarchy is: Entity CPM node --- parent of ---> Subspace CPM node.
         // The Impress node hierarchy is: Subspace Impress node --- parent of ---> Entity Impress
@@ -163,32 +163,32 @@
     }
 
     @Override
-    public CanvasShape getCanvasShape() {
-        return mCanvasShape;
+    public Shape getShape() {
+        return mShape;
     }
 
     @Override
-    public void setCanvasShape(CanvasShape canvasShape) {
-        mCanvasShape = canvasShape;
+    public void setShape(Shape shape) {
+        mShape = shape;
 
-        if (mCanvasShape instanceof CanvasShape.Quad) {
-            CanvasShape.Quad q = (CanvasShape.Quad) mCanvasShape;
+        if (mShape instanceof Shape.Quad) {
+            Shape.Quad q = (Shape.Quad) mShape;
             try {
                 mImpressApi.setStereoSurfaceEntityCanvasShapeQuad(
-                        mEntityImpressNode, q.getWidth(), q.getHeight());
+                        mEntityImpressNode, q.getExtents().getWidth(), q.getExtents().getHeight());
             } catch (IllegalArgumentException e) {
                 throw new IllegalStateException(e);
             }
-        } else if (mCanvasShape instanceof CanvasShape.Vr360Sphere) {
-            CanvasShape.Vr360Sphere s = (CanvasShape.Vr360Sphere) mCanvasShape;
+        } else if (mShape instanceof Shape.Sphere) {
+            Shape.Sphere s = (Shape.Sphere) mShape;
             try {
                 mImpressApi.setStereoSurfaceEntityCanvasShapeSphere(
                         mEntityImpressNode, s.getRadius());
             } catch (IllegalArgumentException e) {
                 throw new IllegalStateException(e);
             }
-        } else if (mCanvasShape instanceof CanvasShape.Vr180Hemisphere) {
-            CanvasShape.Vr180Hemisphere h = (CanvasShape.Vr180Hemisphere) mCanvasShape;
+        } else if (mShape instanceof Shape.Hemisphere) {
+            Shape.Hemisphere h = (Shape.Hemisphere) mShape;
             try {
                 mImpressApi.setStereoSurfaceEntityCanvasShapeHemisphere(
                         mEntityImpressNode, h.getRadius());
@@ -196,22 +196,22 @@
                 throw new IllegalStateException(e);
             }
         } else {
-            throw new IllegalArgumentException("Unsupported canvas shape: " + mCanvasShape);
+            throw new IllegalArgumentException("Unsupported canvas shape: " + mShape);
         }
     }
 
     @Override
     public void setEdgeFeather(EdgeFeather edgeFeather) {
         mEdgeFeather = edgeFeather;
-        if (mEdgeFeather instanceof EdgeFeather.SmoothFeather) {
-            EdgeFeather.SmoothFeather s = (EdgeFeather.SmoothFeather) mEdgeFeather;
+        if (mEdgeFeather instanceof EdgeFeather.RectangleFeather) {
+            EdgeFeather.RectangleFeather s = (EdgeFeather.RectangleFeather) mEdgeFeather;
             try {
                 mImpressApi.setFeatherRadiusForStereoSurface(
                         mEntityImpressNode, s.getLeftRight(), s.getTopBottom());
             } catch (IllegalArgumentException e) {
                 throw new IllegalStateException(e);
             }
-        } else if (mEdgeFeather instanceof EdgeFeather.SolidEdge) {
+        } else if (mEdgeFeather instanceof EdgeFeather.NoFeathering) {
             try {
                 mImpressApi.setFeatherRadiusForStereoSurface(mEntityImpressNode, 0.0f, 0.0f);
             } catch (IllegalArgumentException e) {
@@ -251,7 +251,7 @@
 
     @Override
     public Dimensions getDimensions() {
-        return mCanvasShape.getDimensions();
+        return mShape.getDimensions();
     }
 
     @Override
@@ -322,7 +322,7 @@
     }
 
     @Override
-    public int getMaxCLL() {
+    public int getMaxContentLightLevel() {
         return mMaxContentLightLevel;
     }
 
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/EntityTest.kt b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/EntityTest.kt
index 85ac2f0..bc6d376 100644
--- a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/EntityTest.kt
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/EntityTest.kt
@@ -344,9 +344,9 @@
                 lifecycleManager = lifecycleManager,
                 mockPlatformAdapter,
                 entityManager,
-                SurfaceEntity.StereoMode.SIDE_BY_SIDE,
+                SurfaceEntity.StereoMode.STEREO_MODE_SIDE_BY_SIDE,
                 Pose.Identity,
-                SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
+                SurfaceEntity.Shape.Quad(FloatSize2d(1.0f, 1.0f)),
             )
     }
 
@@ -760,7 +760,7 @@
     @Test
     fun activityPanelEntityLaunchActivity_callsImplLaunchActivity() {
         val launchIntent = Intent(activity.applicationContext, ComponentActivity::class.java)
-        activityPanelEntity.launchActivity(launchIntent, null)
+        activityPanelEntity.startActivity(launchIntent, null)
 
         verify(mockActivityPanelEntity).launchActivity(launchIntent, null)
     }
@@ -1144,14 +1144,14 @@
 
     @Test
     fun SurfaceEntity_redirectsCallsToRtEntity() {
-        surfaceEntity.stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM
-        verify(mockSurfaceEntity).stereoMode = SurfaceEntity.StereoMode.TOP_BOTTOM
+        surfaceEntity.stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM
+        verify(mockSurfaceEntity).stereoMode = SurfaceEntity.StereoMode.STEREO_MODE_TOP_BOTTOM
 
         @Suppress("UNUSED_VARIABLE") var unusedMode = surfaceEntity.stereoMode
         verify(mockSurfaceEntity).stereoMode
 
-        surfaceEntity.canvasShape = SurfaceEntity.CanvasShape.Vr360Sphere(1.0f)
-        verify(mockSurfaceEntity).canvasShape = any()
+        surfaceEntity.shape = SurfaceEntity.Shape.Sphere(1.0f)
+        verify(mockSurfaceEntity).shape = any()
 
         // no equivalent test for getter - that just returns the Kotlin object for now.
     }
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ActivityPanelEntityImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ActivityPanelEntityImplTest.java
index 7e60bed..ff50189 100644
--- a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ActivityPanelEntityImplTest.java
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/ActivityPanelEntityImplTest.java
@@ -162,7 +162,7 @@
     }
 
     @Test
-    public void activityPanelEntityLaunchActivity_callsActivityPanel() {
+    public void activityPanelEntityStartActivity_callsActivityPanel() {
         ActivityPanelEntity activityPanelEntity = createActivityPanelEntity();
         Intent launchIntent = mActivityController.getIntent();
         activityPanelEntity.launchActivity(launchIntent, null);
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java
index 236888b..9cff1b8 100644
--- a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/JxrPlatformAdapterAxrTest.java
@@ -87,6 +87,7 @@
 import androidx.xr.runtime.internal.TextureResource;
 import androidx.xr.runtime.internal.TextureSampler;
 import androidx.xr.runtime.math.Matrix4;
+import androidx.xr.runtime.math.FloatSize2d;
 import androidx.xr.runtime.math.Pose;
 import androidx.xr.runtime.math.Quaternion;
 import androidx.xr.runtime.math.Vector3;
@@ -2301,8 +2302,8 @@
                         mRuntime.createSurfaceEntity(
                                 SurfaceEntity.StereoMode.SIDE_BY_SIDE,
                                 new Pose(),
-                                new SurfaceEntity.CanvasShape.Quad(1.0f, 1.0f),
-                                SurfaceEntity.ContentSecurityLevel.NONE,
+                                new SurfaceEntity.Shape.Quad(new FloatSize2d(1.0f, 1.0f)),
+                                SurfaceEntity.SurfaceProtection.NONE,
                                 SurfaceEntity.SuperSampling.DEFAULT,
                                 mRuntime.getActivitySpaceRootImpl()));
     }
@@ -2318,8 +2319,8 @@
                 mRuntime.createSurfaceEntity(
                         SurfaceEntity.StereoMode.SIDE_BY_SIDE,
                         new Pose(),
-                        new SurfaceEntity.CanvasShape.Quad(kTestWidth, kTestHeight),
-                        SurfaceEntity.ContentSecurityLevel.NONE,
+                        new SurfaceEntity.Shape.Quad(new FloatSize2d(kTestWidth, kTestHeight)),
+                        SurfaceEntity.SurfaceProtection.NONE,
                         SurfaceEntity.SuperSampling.DEFAULT,
                         mRuntime.getActivitySpaceRootImpl());
 
@@ -2334,8 +2335,8 @@
                 mRuntime.createSurfaceEntity(
                         SurfaceEntity.StereoMode.TOP_BOTTOM,
                         new Pose(),
-                        new SurfaceEntity.CanvasShape.Vr360Sphere(kTestSphereRadius),
-                        SurfaceEntity.ContentSecurityLevel.NONE,
+                        new SurfaceEntity.Shape.Sphere(kTestSphereRadius),
+                        SurfaceEntity.SurfaceProtection.NONE,
                         SurfaceEntity.SuperSampling.DEFAULT,
                         mRuntime.getActivitySpaceRootImpl());
 
@@ -2350,8 +2351,8 @@
                 mRuntime.createSurfaceEntity(
                         SurfaceEntity.StereoMode.MONO,
                         new Pose(),
-                        new SurfaceEntity.CanvasShape.Vr180Hemisphere(kTestHemisphereRadius),
-                        SurfaceEntity.ContentSecurityLevel.NONE,
+                        new SurfaceEntity.Shape.Hemisphere(kTestHemisphereRadius),
+                        SurfaceEntity.SurfaceProtection.NONE,
                         SurfaceEntity.SuperSampling.DEFAULT,
                         mRuntime.getActivitySpaceRootImpl());
 
@@ -2399,10 +2400,10 @@
         assertThat(sphereData.getSurface()).isEqualTo(surfaceEntitySphere.getSurface());
         assertThat(hemisphereData.getSurface()).isEqualTo(surfaceEntityHemisphere.getSurface());
 
-        // Check that calls to set the CanvasShape and StereoMode after construction call through
+        // Check that calls to set the Shape and StereoMode after construction call through
         // Change the Quad to a Sphere
-        surfaceEntityQuad.setCanvasShape(
-                new SurfaceEntity.CanvasShape.Vr360Sphere(kTestSphereRadius));
+        surfaceEntityQuad.setShape(
+                new SurfaceEntity.Shape.Sphere(kTestSphereRadius));
         // change the StereoMode to Top/Bottom from Side/Side
         surfaceEntityQuad.setStereoMode(SurfaceEntity.StereoMode.TOP_BOTTOM);
         quadData =
diff --git a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SurfaceEntityImplTest.java b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SurfaceEntityImplTest.java
index a235f1f..5dd136c 100644
--- a/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SurfaceEntityImplTest.java
+++ b/xr/scenecore/scenecore/src/test/java/androidx/xr/scenecore/impl/SurfaceEntityImplTest.java
@@ -31,7 +31,8 @@
 import androidx.xr.runtime.internal.PixelDimensions;
 import androidx.xr.runtime.internal.Space;
 import androidx.xr.runtime.internal.SurfaceEntity;
-import androidx.xr.runtime.internal.SurfaceEntity.CanvasShape;
+import androidx.xr.runtime.internal.SurfaceEntity.Shape;
+import androidx.xr.runtime.math.FloatSize2d;
 import androidx.xr.runtime.math.Pose;
 import androidx.xr.runtime.math.Quaternion;
 import androidx.xr.runtime.math.Vector3;
@@ -77,7 +78,7 @@
     @Before
     public void setUp() {
         mImpressApi = new FakeImpressApiImpl();
-        createDefaultSurfaceEntity(new SurfaceEntity.CanvasShape.Quad(1f, 1f));
+        createDefaultSurfaceEntity(new SurfaceEntity.Shape.Quad(new FloatSize2d(1f, 1f)));
     }
 
     @After
@@ -88,7 +89,7 @@
         }
     }
 
-    private SurfaceEntityImpl createDefaultSurfaceEntity(CanvasShape canvasShape) {
+    private SurfaceEntityImpl createDefaultSurfaceEntity(Shape shape) {
         XrExtensions xrExtensions = XrExtensionsProvider.getXrExtensions();
 
         SplitEngineSubspaceManager splitEngineSubspaceManager =
@@ -121,7 +122,7 @@
 
         int stereoMode = SurfaceEntity.StereoMode.MONO;
         Pose pose = Pose.Identity;
-        int contentSecurityLevel = 0;
+        int surfaceProtection = 0;
         int useSuperSampling = 0;
 
         mSurfaceEntity =
@@ -134,8 +135,8 @@
                         mEntityManager,
                         executor,
                         stereoMode,
-                        canvasShape,
-                        contentSecurityLevel,
+                        shape,
+                        surfaceProtection,
                         useSuperSampling);
         mSurfaceEntity.setPose(pose, Space.PARENT);
 
@@ -162,28 +163,28 @@
 
     @Ignore // b/428211243 this test currently leaks android.view.Surface
     @Test
-    public void setCanvasShape_setsCanvasShape() {
-        SurfaceEntity.CanvasShape expectedCanvasShape =
-                new SurfaceEntity.CanvasShape.Quad(12f, 12f);
-        mSurfaceEntity.setCanvasShape(expectedCanvasShape);
-        SurfaceEntity.CanvasShape canvasShape = mSurfaceEntity.getCanvasShape();
+    public void setShape_setsShape() {
+        SurfaceEntity.Shape expectedShape =
+                new SurfaceEntity.Shape.Quad(new FloatSize2d(12f, 12f));
+        mSurfaceEntity.setShape(expectedShape);
+        SurfaceEntity.Shape shape = mSurfaceEntity.getShape();
 
-        assertThat(canvasShape.getClass()).isEqualTo(expectedCanvasShape.getClass());
-        assertThat(canvasShape.getDimensions()).isEqualTo(expectedCanvasShape.getDimensions());
+        assertThat(shape.getClass()).isEqualTo(expectedShape.getClass());
+        assertThat(shape.getDimensions()).isEqualTo(expectedShape.getDimensions());
 
-        expectedCanvasShape = new SurfaceEntity.CanvasShape.Vr360Sphere(11f);
-        mSurfaceEntity.setCanvasShape(expectedCanvasShape);
-        canvasShape = mSurfaceEntity.getCanvasShape();
+        expectedShape = new SurfaceEntity.Shape.Sphere(11f);
+        mSurfaceEntity.setShape(expectedShape);
+        shape = mSurfaceEntity.getShape();
 
-        assertThat(canvasShape.getClass()).isEqualTo(expectedCanvasShape.getClass());
-        assertThat(canvasShape.getDimensions()).isEqualTo(expectedCanvasShape.getDimensions());
+        assertThat(shape.getClass()).isEqualTo(expectedShape.getClass());
+        assertThat(shape.getDimensions()).isEqualTo(expectedShape.getDimensions());
 
-        expectedCanvasShape = new SurfaceEntity.CanvasShape.Vr180Hemisphere(10f);
-        mSurfaceEntity.setCanvasShape(expectedCanvasShape);
-        canvasShape = mSurfaceEntity.getCanvasShape();
+        expectedShape = new SurfaceEntity.Shape.Hemisphere(10f);
+        mSurfaceEntity.setShape(expectedShape);
+        shape = mSurfaceEntity.getShape();
 
-        assertThat(canvasShape.getClass()).isEqualTo(expectedCanvasShape.getClass());
-        assertThat(canvasShape.getDimensions()).isEqualTo(expectedCanvasShape.getDimensions());
+        assertThat(shape.getClass()).isEqualTo(expectedShape.getClass());
+        assertThat(shape.getDimensions()).isEqualTo(expectedShape.getDimensions());
     }
 
     @Ignore // b/428211243 this test currently leaks android.view.Surface
@@ -205,7 +206,7 @@
     @Ignore // b/428211243 this test currently leaks android.view.Surface
     @Test
     public void dispose_supports_reentry() {
-        CanvasShape.Quad quadShape = new CanvasShape.Quad(1.0f, 1.0f); // 1m x 1m local
+        Shape.Quad quadShape = new Shape.Quad(new FloatSize2d(1.0f, 1.0f)); // 1m x 1m local
         mSurfaceEntity = createDefaultSurfaceEntity(quadShape);
 
         // Note that we don't test that dispose prevents manipulating other properties because that
@@ -220,19 +221,20 @@
         float kFeatherRadiusX = 0.14f;
         float kFeatherRadiusY = 0.28f;
         SurfaceEntity.EdgeFeather expectedFeather =
-                new SurfaceEntity.EdgeFeather.SmoothFeather(kFeatherRadiusX, kFeatherRadiusY);
+            new SurfaceEntity.EdgeFeather.RectangleFeather(kFeatherRadiusX, kFeatherRadiusY);
         mSurfaceEntity.setEdgeFeather(expectedFeather);
         SurfaceEntity.EdgeFeather returnedFeather = mSurfaceEntity.getEdgeFeather();
         assertThat(returnedFeather).isEqualTo(expectedFeather);
 
         // Apply Fake Impress checks
         FakeImpressApiImpl.StereoSurfaceEntityData surfaceEntityData =
-                mImpressApi.getStereoSurfaceEntities().get(mSurfaceEntity.getEntityImpressNode());
+            mImpressApi
+                .getStereoSurfaceEntities().get(mSurfaceEntity.getEntityImpressNode());
         assertThat(surfaceEntityData.getFeatherRadiusX()).isEqualTo(kFeatherRadiusX);
         assertThat(surfaceEntityData.getFeatherRadiusY()).isEqualTo(kFeatherRadiusY);
 
-        // Set back to SolidEdge to simulate turning feathering off
-        expectedFeather = new SurfaceEntity.EdgeFeather.SolidEdge();
+        // Set back to NoFeathering to simulate turning feathering off
+        expectedFeather = new SurfaceEntity.EdgeFeather.NoFeathering();
         mSurfaceEntity.setEdgeFeather(expectedFeather);
         returnedFeather = mSurfaceEntity.getEdgeFeather();
         assertThat(surfaceEntityData.getFeatherRadiusX()).isEqualTo(0.0f);
@@ -250,7 +252,7 @@
     @Ignore // b/428211243 this test currently leaks android.view.Surface
     @Test
     public void getPerceivedResolution_quadInFront_returnsSuccess() {
-        CanvasShape.Quad quadShape = new CanvasShape.Quad(2.0f, 1.0f); // 2m wide, 1m high
+        Shape.Quad quadShape = new Shape.Quad(new FloatSize2d(2.0f, 1.0f)); // 2m wide, 1m high
         // Recreate mSurfaceEntity with the specific shape for this test
         mSurfaceEntity = createDefaultSurfaceEntity(quadShape);
         setupDefaultMockCameraView();
@@ -270,7 +272,7 @@
     @Ignore // b/428211243 this test currently leaks android.view.Surface
     @Test
     public void getPerceivedResolution_sphereInFront_returnsSuccess() {
-        CanvasShape.Vr360Sphere sphereShape = new CanvasShape.Vr360Sphere(1.0f); // radius 1m
+        Shape.Sphere sphereShape = new Shape.Sphere(1.0f); // radius 1m
         mSurfaceEntity = createDefaultSurfaceEntity(sphereShape);
         setupDefaultMockCameraView();
 
@@ -290,7 +292,7 @@
     @Ignore // b/428211243 this test currently leaks android.view.Surface
     @Test
     public void getPerceivedResolution_quadTooClose_returnsEntityTooClose() {
-        CanvasShape.Quad quadShape = new CanvasShape.Quad(2.0f, 1.0f);
+        Shape.Quad quadShape = new Shape.Quad(new FloatSize2d(2.0f, 1.0f));
         mSurfaceEntity = createDefaultSurfaceEntity(quadShape);
         setupDefaultMockCameraView();
 
@@ -306,7 +308,7 @@
     @Ignore // b/428211243 this test currently leaks android.view.Surface
     @Test
     public void getPerceivedResolution_quadWithScale_calculatesCorrectly() {
-        CanvasShape.Quad quadShape = new CanvasShape.Quad(1.0f, 1.0f); // 1m x 1m local
+        Shape.Quad quadShape = new Shape.Quad(new FloatSize2d(1.0f, 1.0f)); // 1m x 1m local
         mSurfaceEntity = createDefaultSurfaceEntity(quadShape);
         setupDefaultMockCameraView();
 
@@ -318,7 +320,7 @@
         PerceivedResolutionResult.Success successResult =
                 (PerceivedResolutionResult.Success) result;
 
-        // The width and height are flipped because perceievedResolution calculations will
+        // The width and height are flipped because perceivedResolution calculations will
         // always place the largest dimension as the width, and the second as height.
         Truth.assertThat(successResult.getPerceivedResolution().width).isEqualTo(750);
         Truth.assertThat(successResult.getPerceivedResolution().height).isEqualTo(500);