Merge "Disable edit text for unchangeable DisplayName (Not part of BluetoothX APIs)" into androidx-main
diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml
index db2e46a..cb49084 100644
--- a/.github/workflows/integration_tests.yml
+++ b/.github/workflows/integration_tests.yml
@@ -25,6 +25,12 @@
           gcp-token: ${{ secrets.GCP_SA_KEY }}
           github-token: ${{ secrets.GITHUB_TOKEN }}
           output-folder: ${{ steps.dirs.outputs.output-dir }}
+          gcp-bucket-name: "androidx-ftl-test-results"
+          gcp-bucket-path: "github-ci-action"
+          log-file: ${{ steps.dirs.outputs.output-dir }}/ftl-logs.txt
+          device-specs: Pixel2.arm:30
+          use-test-config-files: true
+          test-suite-tags: androidx_unit_tests
       - uses: actions/upload-artifact@v2
         if: always()
         with:
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index 7a4f3c0..1a36ae6 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -69,6 +69,8 @@
         configureKtlintCheckFile()
         tasks.register(CheckExternalDependencyLicensesTask.TASK_NAME)
 
+        maybeRegisterFilterableTask()
+
         // If we're running inside Studio, validate the Android Gradle Plugin version.
         val expectedAgpVersion = System.getenv("EXPECTED_AGP_VERSION")
         if (properties.containsKey("android.injected.invoked.from.ide")) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
new file mode 100644
index 0000000..c40a47d
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.build
+
+import androidx.build.FilteredAnchorTask.Companion.GLOBAL_TASK_NAME
+import androidx.build.FilteredAnchorTask.Companion.PROP_PATH_PREFIX
+import androidx.build.FilteredAnchorTask.Companion.PROP_TASK_NAME
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.Project
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.work.DisableCachingByDefault
+
+@DisableCachingByDefault(because = "This is an anchor task that does no work.")
+abstract class FilteredAnchorTask : DefaultTask() {
+    init {
+        group = "Help"
+        description = "Runs tasks with a name specified by -P$PROP_TASK_NAME= for projects with " +
+            "a path prefix specified by -P$PROP_PATH_PREFIX="
+    }
+
+    @get:Input
+    abstract var pathPrefix: String
+
+    @get:Input
+    abstract var taskName: String
+
+    @TaskAction
+    fun exec() {
+        if (dependsOn.isEmpty()) {
+            throw GradleException("Failed to find any filterable tasks with name \"$taskName\" " +
+                "and path prefixed with \"$pathPrefix\"")
+        }
+    }
+
+    companion object {
+        const val GLOBAL_TASK_NAME = "filterTasks"
+        const val PROP_PATH_PREFIX = "androidx.pathPrefix"
+        const val PROP_TASK_NAME = "androidx.taskName"
+    }
+}
+
+/**
+ * Offers the specified [taskProviders] to the global [FilteredAnchorTask], adding them if they match
+ * the requested path prefix and task name.
+ */
+internal fun Project.addFilterableTasks(vararg taskProviders: TaskProvider<*>?) {
+    if (hasProperty(PROP_PATH_PREFIX) && hasProperty(PROP_TASK_NAME)) {
+        val pathPrefix = properties[PROP_PATH_PREFIX] as String
+        if (relativePathForFiltering().startsWith(pathPrefix)) {
+            val taskName = properties[PROP_TASK_NAME] as String
+            taskProviders.find { taskProvider ->
+                taskName == taskProvider?.name
+            }?.let { taskProvider ->
+                rootProject.tasks.named(GLOBAL_TASK_NAME).configure { task ->
+                    task.dependsOn(taskProvider)
+                }
+            }
+        }
+    }
+}
+
+/**
+ * Registers the global [FilteredAnchorTask] if the required command-line properties are set.
+ *
+ * For example, to run `checkApi` for all projects under `core/core/`:
+ * ./gradlew filterTasks -Pandroidx.taskName=checkApi -Pandroidx.pathPrefix=core/core/
+ */
+internal fun Project.maybeRegisterFilterableTask() {
+    if (hasProperty(PROP_TASK_NAME) && hasProperty(PROP_PATH_PREFIX)) {
+        tasks.register(GLOBAL_TASK_NAME, FilteredAnchorTask::class.java) { task ->
+            task.pathPrefix = properties[PROP_PATH_PREFIX] as String
+            task.taskName = properties[PROP_TASK_NAME] as String
+        }
+    }
+}
+
+/**
+ * Returns an AndroidX-relative path for the [Project], inserting the root project directory when
+ * run in a Playground context such that paths are consistent with the AndroidX context.
+ */
+internal fun Project.relativePathForFiltering(): String =
+    if (ProjectLayoutType.isPlayground(project)) {
+        "${rootProject.projectDir.name}/"
+    } else {
+        ""
+    } + "${projectDir.relativeTo(rootDir)}/"
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
index d7c4c07..54597b4 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
@@ -258,12 +258,7 @@
          * of the build that is released.  Thus, we use frameworks/support to get the sha
          */
         fun Project.getFrameworksSupportCommitShaAtHead(): String {
-            val gitClient = GitClient.create(
-                project.getSupportRootFolder(),
-                logger,
-                GitClient.getChangeInfoPath(project).get(),
-                GitClient.getManifestPath(project).get()
-            )
+            val gitClient = GitClient.forProject(project)
             return gitClient.getHeadSha(getSupportRootFolder())
         }
     }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt b/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
index feb0a6f..756da81 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
@@ -278,7 +278,7 @@
                 logger.info("using base commit override $baseCommitOverride")
             }
             val gitClient = GitClient.create(
-                rootProjectDir = parameters.rootDir,
+                projectDir = parameters.rootDir,
                 logger = logger.toLogger(),
                 changeInfoPath = parameters.changeInfoPath.get(),
                 manifestPath = parameters.manifestPath.get()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaCombinedDocsTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaCombinedDocsTask.kt
index 471f537..bfec803 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaCombinedDocsTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaCombinedDocsTask.kt
@@ -204,12 +204,7 @@
                 it.additionalDocumentation.set(
                     project.files("homepage.md")
                 )
-                val gitClient = GitClient.create(
-                    project.getSupportRootFolder(),
-                    project.logger,
-                    GitClient.getChangeInfoPath(project).get(),
-                    GitClient.getManifestPath(project).get()
-                )
+                val gitClient = GitClient.forProject(project)
                 it.replacementUrl.set(
                     DokkaUtils.createCsAndroidUrl(
                         gitClient.getHeadSha(project.getSupportRootFolder())
@@ -253,4 +248,4 @@
             }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt b/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
index 9b1ecee..25c6a81 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitClient.kt
@@ -19,6 +19,7 @@
 import androidx.build.releasenotes.getBuganizerLink
 import androidx.build.releasenotes.getChangeIdAOSPLink
 import java.io.File
+import java.util.concurrent.ConcurrentHashMap
 import org.gradle.api.GradleException
 import org.gradle.api.Project
 import org.gradle.api.logging.Logger
@@ -83,8 +84,16 @@
         fun getManifestPath(project: Project): Provider<String> {
             return project.providers.environmentVariable("MANIFEST").orElse("")
         }
+        fun forProject(project: Project): GitClient {
+            return create(
+                project.projectDir,
+                project.logger,
+                GitClient.getChangeInfoPath(project).get(),
+                GitClient.getManifestPath(project).get()
+            )
+        }
         fun create(
-            rootProjectDir: File,
+            projectDir: File,
             logger: Logger,
             changeInfoPath: String,
             manifestPath: String
@@ -107,8 +116,41 @@
                     "manifest $manifestPath")
                 return ChangeInfoGitClient(changeInfoText, manifestText)
             }
+            val gitRoot = findGitDirInParentFilepath(projectDir)
+            check(gitRoot != null) {
+                "Could not find .git dir for $projectDir"
+            }
             logger.info("UsingGitRunnerGitClient")
-            return GitRunnerGitClient(rootProjectDir, logger)
+            return GitRunnerGitClient(gitRoot, logger)
+        }
+    }
+}
+
+data class MultiGitClient(
+    val logger: Logger,
+    val changeInfoPath: String,
+    val manifestPath: String
+) {
+    // Map from the root of the git repository to a GitClient for that repository
+    // In AndroidX this directory could be frameworks/support, external/noto-fonts, or others
+    @Transient // We don't want Gradle to persist GitClient in the configuration cache
+    val cache: MutableMap<File, GitClient> = ConcurrentHashMap()
+
+    fun getGitClient(projectDir: File): GitClient {
+        return cache.getOrPut(
+            key = projectDir
+        ) {
+            GitClient.create(projectDir, logger, changeInfoPath, manifestPath)
+        }
+    }
+
+    companion object {
+        fun create(project: Project): MultiGitClient {
+            return MultiGitClient(
+                project.logger,
+                GitClient.getChangeInfoPath(project).get(),
+                GitClient.getManifestPath(project).get()
+            )
         }
     }
 }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitRunnerGitClient.kt b/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitRunnerGitClient.kt
index 2057187..342d2e5 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitRunnerGitClient.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/gitclient/GitRunnerGitClient.kt
@@ -66,17 +66,6 @@
             ?.firstOrNull()
     }
 
-    private fun findGitDirInParentFilepath(filepath: File): File? {
-        var curDirectory: File = filepath
-        while (curDirectory.path != "/") {
-            if (File("$curDirectory/.git").exists()) {
-                return curDirectory
-            }
-            curDirectory = curDirectory.parentFile
-        }
-        return null
-    }
-
     private fun parseCommitLogString(
         commitLogString: String,
         commitStartDelimiter: String,
@@ -214,3 +203,17 @@
         const val GIT_LOG_CMD_PREFIX = "git log --name-only"
     }
 }
+
+/**
+ * Finds the git directory containing the given File by checking parent directories
+ */
+internal fun findGitDirInParentFilepath(filepath: File): File? {
+    var curDirectory: File = filepath
+    while (curDirectory.path != "/") {
+        if (File("$curDirectory/.git").exists()) {
+            return curDirectory
+        }
+        curDirectory = curDirectory.parentFile
+    }
+    return null
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt
index c8458bc..63e1edb 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt
@@ -17,7 +17,7 @@
 package androidx.build.metalava
 
 import androidx.build.AndroidXExtension
-import androidx.build.ProjectLayoutType.Companion.isPlayground
+import androidx.build.addFilterableTasks
 import androidx.build.addToBuildOnServer
 import androidx.build.addToCheckTask
 import androidx.build.checkapi.ApiBaselinesLocation
@@ -26,6 +26,7 @@
 import androidx.build.getSuppressCompatibilityOptInPathPrefixes
 import androidx.build.getSuppressCompatibilityOptOutPathPrefixes
 import androidx.build.java.JavaCompileInputs
+import androidx.build.relativePathForFiltering
 import androidx.build.uptodatedness.cacheEvenIfNoOutputs
 import com.android.build.gradle.tasks.ProcessLibraryManifest
 import org.gradle.api.Project
@@ -113,7 +114,7 @@
             }
         }
 
-        project.tasks.register(
+        val updateApiLintBaseline = project.tasks.register(
             "updateApiLintBaseline",
             UpdateApiLintBaselineTask::class.java
         ) { task ->
@@ -189,7 +190,7 @@
         // Make sure it always runs *after* the updateApi task.
         ignoreApiChanges?.configure { it.mustRunAfter(updateApi) }
 
-        project.tasks.register("regenerateApis") { task ->
+        val regenerateApis = project.tasks.register("regenerateApis") { task ->
             task.group = "API"
             task.description = "Regenerates current and historic API .txt files using the " +
                 "corresponding prebuilt and the latest Metalava, then updates API ignore files"
@@ -200,6 +201,14 @@
 
         project.addToCheckTask(checkApi)
         project.addToBuildOnServer(checkApi)
+        project.addFilterableTasks(
+            ignoreApiChanges,
+            updateApiLintBaseline,
+            checkApi,
+            regenerateOldApis,
+            updateApi,
+            regenerateApis,
+        )
     }
 
     private fun applyInputs(inputs: JavaCompileInputs, task: MetalavaTask) {
@@ -214,11 +223,7 @@
  * Returns whether the project has been opted-in to the Suppress Compatibility migration.
  */
 internal fun Project.isOptedInToSuppressCompatibilityMigration(): Boolean {
-    val dir = if (isPlayground(project)) {
-        "${rootProject.projectDir.name}/"
-    } else {
-        ""
-    } + "${projectDir.relativeTo(rootDir)}/"
+    val dir = relativePathForFiltering()
     return getSuppressCompatibilityOptOutPathPrefixes().none { pathPrefix ->
         dir.startsWith(pathPrefix)
     } && getSuppressCompatibilityOptInPathPrefixes().any { pathPrefix ->
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt b/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
index 6849ca4..8889212 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/sbom/Sbom.kt
@@ -37,7 +37,11 @@
     return when (configurationName) {
         BundleInsideHelper.CONFIGURATION_NAME -> true
         "shadowed" -> true
-        "compileClasspath" -> appliesShadowPlugin()
+        // compileClasspath is included by the Shadow plugin by default but projects that
+        // declare a "shadowed" configuration exclude the "compileClasspath" configuration from
+        // the shadowJar task
+        "compileClasspath" ->
+            appliesShadowPlugin() && project.configurations.findByName("shadowed") == null
         EXPORT_INSPECTOR_DEPENDENCIES -> true
         IMPORT_INSPECTOR_DEPENDENCIES -> true
         else -> false
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
index b6c5c29..51a2301 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
@@ -36,17 +36,33 @@
  *
  * The possible values of LibraryType are as follows:
  * PUBLISHED_LIBRARY: a conventional library, published, sourced, documented, and versioned.
- * SAMPLES: a library of samples, published as additional properties to a conventional library,
- *      including published source. Documented in a special way, not API tracked.
+ * PUBLISHED_TEST_LIBRARY: PUBLISHED_LIBRARY, but allows calling @VisibleForTesting API. Used for
+ * libraries that allow developers to test code that uses your library. Often provides test fakes.
+ * PUBLISHED_NATIVE_LIBRARY: PUBLISHED_LIBRARY, but uses native API tracking instead of Java
+ * INTERNAL_TEST_LIBRARY: unpublished, untracked, undocumented. Used in internal tests. Usually
+ * contains integration tests, but is _not_ an app. Runs device tests.
+ * INTERNAL_HOST_TEST_LIBRARY: as INTERNAL_TEST_LIBRARY, but runs host tests instead. Avoid mixing
+ * host tests and device tests in the same library, for performance / test-result-caching reasons.
+ * SAMPLES: a library containing sample code referenced in your library's documentation with
+ * @sampled, published as a documentation-related supplement to a conventional library.
  * LINT: a library of lint rules for using a conventional library. Published through lintPublish as
- *      part of an AAR, not published standalone.
- * COMPILER_PLUGIN: a tool that modifies the kotlin or java compiler. Used only while compiling.
+ * part of an AAR, not published standalone.
+ * COMPILER_DAEMON: a tool that modifies the kotlin or java compiler. Used only while compiling. Has
+ * no API and does not publish source jars, but does release to maven.
+ * COMPILER_DAEMON_TEST: a compiler plugin that is not published at all, for internal-only use.
+ * COMPILER_PLUGIN: as COMPILER_DAEMON, but is compatible with JDK 11.
  * GRADLE_PLUGIN: a library that is a gradle plugin.
  * ANNOTATION_PROCESSOR: a library consisting of an annotation processor. Used only while compiling.
+ * ANNOTATION_PROCESSOR_UTILS: contains reference code for understanding an annotation processor.
+ * Publishes source jars, but does not track API.
  * OTHER_CODE_PROCESSOR: a library that algorithmically generates and/or alters code
  *                      but not through hooking into custom annotations or the kotlin compiler.
  *                      For example, navigation:safe-args-generator or Jetifier.
+ * IDE_PLUGIN: a library that should only ever be downloaded by studio. Unfortunately, we don't
+ * yet have a good way to track API for these. b/281843422
  * UNSET: a library that has not yet been migrated to using LibraryType. Should never be used.
+ * APP: an app, such as an example app or integration testsapp. Should never be used; apps should
+ * not apply the AndroidX plugin or have an androidx block in their build.gradle files.
  *
  * TODO: potential future LibraryTypes:
  * KOTLIN_ONLY_LIBRARY: like PUBLISHED_LIBRARY, but not intended for use from java. ktx and compose.
@@ -82,7 +98,10 @@
         val OTHER_CODE_PROCESSOR = OtherCodeProcessor()
         val IDE_PLUGIN = IdePlugin()
         val UNSET = Unset()
+        @Deprecated("Do not use an androidx block for apps/testapps, only for libraries")
+        val APP = UNSET
 
+        @Suppress("DEPRECATION")
         private val allTypes = mapOf(
             "PUBLISHED_LIBRARY" to PUBLISHED_LIBRARY,
             "PUBLISHED_TEST_LIBRARY" to PUBLISHED_TEST_LIBRARY,
@@ -99,7 +118,8 @@
             "ANNOTATION_PROCESSOR_UTILS" to ANNOTATION_PROCESSOR_UTILS,
             "OTHER_CODE_PROCESSOR" to OTHER_CODE_PROCESSOR,
             "IDE_PLUGIN" to IDE_PLUGIN,
-            "UNSET" to UNSET
+            "UNSET" to UNSET,
+            "APP" to APP
         )
         fun valueOf(name: String): LibraryType {
             val result = allTypes[name]
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt
index 0a60ca7..7a88d02 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/Camera2InteropIntegrationTest.kt
@@ -156,9 +156,10 @@
                 when (state) {
                     // Filter out this state from the downstream flow
                     is SessionState.Unknown -> true
-                    is SessionState.Configured -> {
+                    is SessionState.Configured -> true
+                    is SessionState.Ready -> {
                         withContext(Dispatchers.Main) { processCameraProvider!!.unbindAll() }
-                        true // Filter out this state from the downstream flow
+                        false
                     }
 
                     else -> false // Forward to the downstream flow
@@ -368,19 +369,19 @@
         MutableStateFlow<SessionState>(SessionState.Unknown).apply {
             val stateCallback = object : CameraCaptureSession.StateCallback() {
                 override fun onReady(session: CameraCaptureSession) {
-                    tryEmit(SessionState.Ready)
+                        tryEmit(SessionState.Ready)
                 }
 
                 override fun onConfigured(session: CameraCaptureSession) {
-                    tryEmit(SessionState.Configured)
+                        tryEmit(SessionState.Configured)
                 }
 
                 override fun onConfigureFailed(session: CameraCaptureSession) {
-                    tryEmit(SessionState.ConfigureFailed)
+                        tryEmit(SessionState.ConfigureFailed)
                 }
 
                 override fun onClosed(session: CameraCaptureSession) {
-                    tryEmit(SessionState.Closed)
+                        tryEmit(SessionState.Closed)
                 }
             }
             CameraPipeUtil.setSessionStateCallback(
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
index d666189..1c4e841 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
@@ -71,6 +71,7 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsProperties
 import androidx.compose.ui.semantics.error
+import androidx.compose.ui.semantics.getOrNull
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
@@ -79,6 +80,7 @@
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.click
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChildren
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
@@ -1080,19 +1082,32 @@
     @Test
     fun testErrorSemantics_messageOverridable() {
         val errorMessage = "Special symbols not allowed"
+        lateinit var defaultErrorMessage: String
         rule.setMaterialContent {
             var isError = remember { mutableStateOf(true) }
             OutlinedTextField(
                 value = "test",
                 onValueChange = {},
-                modifier = Modifier.semantics { if (isError.value) error(errorMessage) },
+                modifier = Modifier
+                    .testTag(TextfieldTag)
+                    .semantics { if (isError.value) error(errorMessage) },
                 isError = isError.value
             )
+            defaultErrorMessage = getString(Strings.DefaultErrorMessage)
         }
 
-        rule.onNodeWithText("test")
+        rule.onNodeWithTag(TextfieldTag)
             .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Error))
             .assert(SemanticsMatcher.expectValue(SemanticsProperties.Error, errorMessage))
+
+        // Check that default error message is overwritten and not lingering in a child node
+        rule.onNodeWithTag(TextfieldTag, useUnmergedTree = true)
+            .onChildren()
+            .fetchSemanticsNodes()
+            .forEach { node ->
+                assertThat(node.config.getOrNull(SemanticsProperties.Error))
+                    .isNotEqualTo(defaultErrorMessage)
+            }
     }
 
     @Test
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
index e57bdd9..214cfb7 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
@@ -83,6 +83,7 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsProperties
 import androidx.compose.ui.semantics.error
+import androidx.compose.ui.semantics.getOrNull
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
@@ -91,6 +92,7 @@
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.click
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChildren
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
@@ -1235,19 +1237,32 @@
     @Test
     fun testErrorSemantics_messageOverridable() {
         val errorMessage = "Special symbols not allowed"
+        lateinit var defaultErrorMessage: String
         rule.setMaterialContent {
-            var isError = remember { mutableStateOf(true) }
+            val isError = remember { mutableStateOf(true) }
             TextField(
                 value = "test",
                 onValueChange = {},
-                modifier = Modifier.semantics { if (isError.value) error(errorMessage) },
+                modifier = Modifier
+                    .testTag(TextfieldTag)
+                    .semantics { if (isError.value) error(errorMessage) },
                 isError = isError.value
             )
+            defaultErrorMessage = getString(DefaultErrorMessage)
         }
 
-        rule.onNodeWithText("test")
+        rule.onNodeWithTag(TextfieldTag)
             .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Error))
             .assert(SemanticsMatcher.expectValue(SemanticsProperties.Error, errorMessage))
+
+        // Check that default error message is overwritten and not lingering in a child node
+        rule.onNodeWithTag(TextfieldTag, useUnmergedTree = true)
+            .onChildren()
+            .fetchSemanticsNodes()
+            .forEach { node ->
+                assertThat(node.config.getOrNull(SemanticsProperties.Error))
+                    .isNotEqualTo(defaultErrorMessage)
+            }
     }
 
     @Test
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
index 5f3120a..8e90c16 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
@@ -172,6 +172,7 @@
             modifier
         }
             .background(colors.backgroundColor(enabled).value, shape)
+            .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
             .defaultMinSize(
                 minWidth = TextFieldDefaults.MinWidth,
                 minHeight = TextFieldDefaults.MinHeight
@@ -373,6 +374,7 @@
             modifier
         }
             .background(colors.backgroundColor(enabled).value, shape)
+            .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
             .defaultMinSize(
                 minWidth = TextFieldDefaults.MinWidth,
                 minHeight = TextFieldDefaults.MinHeight
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
index dfbd2c8..984e496 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
@@ -191,6 +191,7 @@
         modifier = modifier
             .background(colors.backgroundColor(enabled).value, shape)
             .indicatorLine(enabled, isError, interactionSource, colors)
+            .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
             .defaultMinSize(
                 minWidth = TextFieldDefaults.MinWidth,
                 minHeight = TextFieldDefaults.MinHeight
@@ -379,6 +380,7 @@
         modifier = modifier
             .background(colors.backgroundColor(enabled).value, shape)
             .indicatorLine(enabled, isError, interactionSource, colors)
+            .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
             .defaultMinSize(
                 minWidth = TextFieldDefaults.MinWidth,
                 minHeight = TextFieldDefaults.MinHeight
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt
index f46c5b1..c947d0a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextFieldImpl.kt
@@ -27,7 +27,6 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.defaultMinSize
-import androidx.compose.material.Strings.Companion.DefaultErrorMessage
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.ComposableOpenTarget
 import androidx.compose.runtime.CompositionLocalProvider
@@ -146,12 +145,6 @@
                 }
             } else null
 
-        // Developers need to handle invalid input manually. But since we don't provide error
-        // message slot API, we can set the default error message in case developers forget about
-        // it.
-        val defaultErrorMessage = getString(DefaultErrorMessage)
-        val decorationBoxModifier = Modifier.semantics { if (isError) error(defaultErrorMessage) }
-
         val leadingIconColor = if (colors is TextFieldColorsWithIcons) {
             colors.leadingIconColor(enabled, isError, interactionSource).value
         } else {
@@ -177,7 +170,7 @@
         when (type) {
             TextFieldType.Filled -> {
                 TextFieldLayout(
-                    modifier = decorationBoxModifier,
+                    modifier = Modifier,
                     textField = innerTextField,
                     placeholder = decoratedPlaceholder,
                     label = decoratedLabel,
@@ -201,7 +194,7 @@
                 }
 
                 OutlinedTextFieldLayout(
-                    modifier = decorationBoxModifier,
+                    modifier = Modifier,
                     textField = innerTextField,
                     placeholder = decoratedPlaceholder,
                     label = decoratedLabel,
@@ -255,6 +248,13 @@
     if (typography != null) ProvideTextStyle(typography, colorAndEmphasis) else colorAndEmphasis()
 }
 
+// Developers need to handle invalid input manually. But since we don't provide an error message
+// slot API, we can set the default error message in case developers forget about it.
+internal fun Modifier.defaultErrorSemantics(
+    isError: Boolean,
+    defaultErrorMessage: String,
+): Modifier = if (isError) semantics { error(defaultErrorMessage) } else this
+
 internal fun widthOrZero(placeable: Placeable?) = placeable?.width ?: 0
 internal fun heightOrZero(placeable: Placeable?) = placeable?.height ?: 0
 
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt
index c8a4298..fddbdf2a 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldTest.kt
@@ -52,6 +52,7 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsProperties
 import androidx.compose.ui.semantics.error
+import androidx.compose.ui.semantics.getOrNull
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
@@ -60,6 +61,7 @@
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.click
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChildren
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
@@ -1362,19 +1364,32 @@
     @Test
     fun testOutlinedTextField_errorSemantics_messageOverridable() {
         val errorMessage = "Special symbols not allowed"
+        lateinit var defaultErrorMessage: String
         rule.setMaterialContent(lightColorScheme()) {
             val isError = remember { mutableStateOf(true) }
             OutlinedTextField(
                 value = "test",
                 onValueChange = {},
-                modifier = Modifier.semantics { if (isError.value) error(errorMessage) },
+                modifier = Modifier
+                    .testTag(TextFieldTag)
+                    .semantics { if (isError.value) error(errorMessage) },
                 isError = isError.value
             )
+            defaultErrorMessage = getString(Strings.DefaultErrorMessage)
         }
 
-        rule.onNodeWithText("test")
+        rule.onNodeWithTag(TextFieldTag)
             .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Error))
             .assert(SemanticsMatcher.expectValue(SemanticsProperties.Error, errorMessage))
+
+        // Check that default error message is overwritten and not lingering in a child node
+        rule.onNodeWithTag(TextFieldTag, useUnmergedTree = true)
+            .onChildren()
+            .fetchSemanticsNodes()
+            .forEach { node ->
+                assertThat(node.config.getOrNull(SemanticsProperties.Error))
+                    .isNotEqualTo(defaultErrorMessage)
+            }
     }
 
     @Test
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldTest.kt
index ef94485..e1b215d 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TextFieldTest.kt
@@ -65,6 +65,7 @@
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsProperties
 import androidx.compose.ui.semantics.error
+import androidx.compose.ui.semantics.getOrNull
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.test.SemanticsMatcher
 import androidx.compose.ui.test.assert
@@ -74,6 +75,7 @@
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.click
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChildren
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
@@ -1466,19 +1468,32 @@
     @Test
     fun testTextField_errorSemantics_messageOverridable() {
         val errorMessage = "Special symbols not allowed"
+        lateinit var defaultErrorMessage: String
         rule.setMaterialContent(lightColorScheme()) {
             val isError = remember { mutableStateOf(true) }
             TextField(
                 value = "test",
                 onValueChange = {},
-                modifier = Modifier.semantics { if (isError.value) error(errorMessage) },
+                modifier = Modifier
+                    .testTag(TextFieldTag)
+                    .semantics { if (isError.value) error(errorMessage) },
                 isError = isError.value
             )
+            defaultErrorMessage = getString(DefaultErrorMessage)
         }
 
-        rule.onNodeWithText("test")
+        rule.onNodeWithTag(TextFieldTag)
             .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Error))
             .assert(SemanticsMatcher.expectValue(SemanticsProperties.Error, errorMessage))
+
+        // Check that default error message is overwritten and not lingering in a child node
+        rule.onNodeWithTag(TextFieldTag, useUnmergedTree = true)
+            .onChildren()
+            .fetchSemanticsNodes()
+            .forEach { node ->
+                assertThat(node.config.getOrNull(SemanticsProperties.Error))
+                    .isNotEqualTo(defaultErrorMessage)
+            }
     }
 
     @Test
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
index 630f24b..3d47224 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
@@ -180,6 +180,7 @@
             } else {
                 modifier
             }
+                .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
                 .defaultMinSize(
                     minWidth = OutlinedTextFieldDefaults.MinWidth,
                     minHeight = OutlinedTextFieldDefaults.MinHeight
@@ -339,6 +340,7 @@
             } else {
                 modifier
             }
+                .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
                 .defaultMinSize(
                     minWidth = OutlinedTextFieldDefaults.MinWidth,
                     minHeight = OutlinedTextFieldDefaults.MinHeight
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
index cd786c6..039d06c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
@@ -198,6 +198,7 @@
         BasicTextField(
             value = value,
             modifier = modifier
+                .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
                 .defaultMinSize(
                     minWidth = TextFieldDefaults.MinWidth,
                     minHeight = TextFieldDefaults.MinHeight
@@ -343,6 +344,7 @@
         BasicTextField(
             value = value,
             modifier = modifier
+                .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
                 .defaultMinSize(
                     minWidth = TextFieldDefaults.MinWidth,
                     minHeight = TextFieldDefaults.MinHeight
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldImpl.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldImpl.kt
index 6cfb01f..2ee2a6c 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldImpl.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextFieldImpl.kt
@@ -27,7 +27,6 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.defaultMinSize
-import androidx.compose.material3.Strings.Companion.DefaultErrorMessage
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.getValue
@@ -172,12 +171,6 @@
                 }
             } else null
 
-        // Developers need to handle invalid input manually. But since we don't provide error
-        // message slot API, we can set the default error message in case developers forget about
-        // it.
-        val defaultErrorMessage = getString(DefaultErrorMessage)
-        val decorationBoxModifier = Modifier.semantics { if (isError) error(defaultErrorMessage) }
-
         val leadingIconColor = colors.leadingIconColor(enabled, isError, interactionSource).value
         val decoratedLeading: @Composable (() -> Unit)? = leadingIcon?.let {
             @Composable {
@@ -210,7 +203,7 @@
                 }
 
                 TextFieldLayout(
-                    modifier = decorationBoxModifier,
+                    modifier = Modifier,
                     textField = innerTextField,
                     placeholder = decoratedPlaceholder,
                     label = decoratedLabel,
@@ -240,7 +233,7 @@
                 }
 
                 OutlinedTextFieldLayout(
-                    modifier = decorationBoxModifier,
+                    modifier = Modifier,
                     textField = innerTextField,
                     placeholder = decoratedPlaceholder,
                     label = decoratedLabel,
@@ -286,6 +279,13 @@
     if (typography != null) ProvideTextStyle(typography, contentWithColor) else contentWithColor()
 }
 
+// Developers need to handle invalid input manually. But since we don't provide an error message
+// slot API, we can set the default error message in case developers forget about it.
+internal fun Modifier.defaultErrorSemantics(
+    isError: Boolean,
+    defaultErrorMessage: String,
+): Modifier = if (isError) semantics { error(defaultErrorMessage) } else this
+
 internal fun widthOrZero(placeable: Placeable?) = placeable?.width ?: 0
 internal fun heightOrZero(placeable: Placeable?) = placeable?.height ?: 0
 
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 044964f..23a1cff 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2363,22 +2363,9 @@
     method @androidx.compose.runtime.Stable public static operator long times(long, long size);
   }
 
-  @androidx.compose.ui.ExperimentalComposeUiApi public sealed interface SubcomposeIntermediateMeasureScope extends androidx.compose.ui.layout.SubcomposeMeasureScope {
-    method public long getLookaheadConstraints();
-    method public kotlin.jvm.functions.Function2<androidx.compose.ui.layout.SubcomposeMeasureScope,androidx.compose.ui.unit.Constraints,androidx.compose.ui.layout.MeasureResult> getLookaheadMeasurePolicy();
-    method public long getLookaheadSize();
-    method public java.util.List<androidx.compose.ui.layout.Measurable> measurablesForSlot(Object? slotId);
-    method public default java.util.List<androidx.compose.ui.layout.Measurable> subcompose(Object? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    property public abstract long lookaheadConstraints;
-    property public abstract kotlin.jvm.functions.Function2<androidx.compose.ui.layout.SubcomposeMeasureScope,androidx.compose.ui.unit.Constraints,androidx.compose.ui.layout.MeasureResult> lookaheadMeasurePolicy;
-    property public abstract long lookaheadSize;
-  }
-
   public final class SubcomposeLayoutKt {
     method @androidx.compose.runtime.Composable public static void SubcomposeLayout(optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
-    method @androidx.compose.runtime.Composable @androidx.compose.ui.ExperimentalComposeUiApi public static void SubcomposeLayout(optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeIntermediateMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> intermediateMeasurePolicy, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
     method @androidx.compose.runtime.Composable @androidx.compose.ui.UiComposable public static void SubcomposeLayout(androidx.compose.ui.layout.SubcomposeLayoutState state, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
-    method @androidx.compose.runtime.Composable @androidx.compose.ui.ExperimentalComposeUiApi @androidx.compose.ui.UiComposable public static void SubcomposeLayout(androidx.compose.ui.layout.SubcomposeLayoutState state, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeIntermediateMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> intermediateMeasurePolicy, kotlin.jvm.functions.Function2<? super androidx.compose.ui.layout.SubcomposeMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
     method public static androidx.compose.ui.layout.SubcomposeSlotReusePolicy SubcomposeSlotReusePolicy(int maxSlotsToRetainForReuse);
   }
 
@@ -2386,9 +2373,7 @@
     ctor public SubcomposeLayoutState(androidx.compose.ui.layout.SubcomposeSlotReusePolicy slotReusePolicy);
     ctor public SubcomposeLayoutState();
     ctor @Deprecated public SubcomposeLayoutState(int maxSlotsToRetainForReuse);
-    method @androidx.compose.ui.ExperimentalComposeUiApi public boolean isInLookaheadScope();
     method public androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle precompose(Object? slotId, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    property @androidx.compose.ui.ExperimentalComposeUiApi public final boolean isInLookaheadScope;
   }
 
   public static interface SubcomposeLayoutState.PrecomposedSlotHandle {
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/SubcomposeLayoutSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/SubcomposeLayoutSample.kt
index 9c932f2..b116741 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/SubcomposeLayoutSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/SubcomposeLayoutSample.kt
@@ -17,27 +17,9 @@
 package androidx.compose.ui.samples
 
 import androidx.annotation.Sampled
-import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.AnimationVector2D
-import androidx.compose.animation.core.VectorConverter
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.UiComposable
-import androidx.compose.ui.composed
-import androidx.compose.ui.layout.MeasurePolicy
 import androidx.compose.ui.layout.SubcomposeLayout
-import androidx.compose.ui.layout.intermediateLayout
-import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.launch
 
 @Sampled
 @Composable
@@ -67,97 +49,4 @@
     }
 }
 
-enum class SlotsEnum { Main, Dependent }
-
-@OptIn(ExperimentalComposeUiApi::class)
-@Sampled
-fun SubcomposeLayoutWithIntermediateMeasurePolicySample() {
-    // In this example, there is a custom modifier that animates the constraints and measures
-    // child with the animated constraints, as defined below.
-    // This modifier is built on top of `Modifier.intermediateLayout`, which
-    // allows access to the lookahead size of the layout. A resize animation will be kicked off
-    // whenever the lookahead size changes, to animate children from current size to lookahead size.
-    // Fixed constraints created based on the animation value will be used to measure
-    // child, so the child layout gradually changes its size and potentially its child's placement
-    // to fit within the animated constraints.
-    fun Modifier.animateConstraints() = composed {
-        // Creates a size animation
-        var sizeAnimation: Animatable<IntSize, AnimationVector2D>? by remember {
-            mutableStateOf(null)
-        }
-
-        this.intermediateLayout { measurable, _ ->
-            // When layout changes, the lookahead pass will calculate a new final size for the
-            // child layout. This lookahead size can be used to animate the size
-            // change, such that the animation starts from the current size and gradually
-            // change towards `lookaheadSize`.
-            if (lookaheadSize != sizeAnimation?.targetValue) {
-                sizeAnimation?.run {
-                    launch { animateTo(lookaheadSize) }
-                } ?: Animatable(lookaheadSize, IntSize.VectorConverter).let {
-                    sizeAnimation = it
-                }
-            }
-            val (width, height) = sizeAnimation!!.value
-            // Creates a fixed set of constraints using the animated size
-            val animatedConstraints = Constraints.fixed(width, height)
-            // Measure child with animated constraints.
-            val placeable = measurable.measure(animatedConstraints)
-            layout(placeable.width, placeable.height) {
-                placeable.place(0, 0)
-            }
-        }
-    }
-
-    // In the example below, the SubcomposeLayout has a parent layout that animates its width
-    // between two fixed sizes using the `animateConstraints` modifier we created above.
-    @Composable
-    fun SubcomposeLayoutWithAnimatingParentLayout(
-        isWide: Boolean,
-        modifier: Modifier = Modifier,
-        content: @Composable @UiComposable () -> Unit
-    ) {
-        // Create a MeasurePolicy to measure all children with incoming constraints and return the
-        // largest width & height.
-        val myMeasurePolicy = MeasurePolicy { measurables, constraints ->
-            val placeables = measurables.map { it.measure(constraints) }
-            val maxWidth = placeables.maxOf { it.width }
-            val maxHeight = placeables.maxOf { it.height }
-            layout(maxWidth, maxHeight) {
-                placeables.forEach { it.place(0, 0) }
-            }
-        }
-        Box(
-            Modifier
-                .requiredSize(if (isWide) 400.dp else 200.dp)
-                .animateConstraints()
-        ) {
-            // SubcomposeLayout's measurePolicy will only be invoked with lookahead constraints.
-            // The parent layout in this example is animating between two fixed widths. The
-            // [measurePolicy] parameter will only be called with lookahead constraints
-            // (i.e. constraints for 400.dp x 400.dp or 200.dp x 200.dp depending on the state.)
-            // This may cause content lambda to jump to its final size. To create a smooth
-            // experience, we need to remeasure the content with the intermediate
-            // constraints created by the `animateConstraints` that we built above. Therefore, we
-            // need to provide a [intermediateMeasurePolicy] to define how to measure the
-            // content (using the measureables of the content that was composed in [measurePolicy])
-            // with intermediate constraints.
-            SubcomposeLayout(
-                modifier,
-                intermediateMeasurePolicy = { intermediateConstraints ->
-                    // Retrieve the measureables for slotId = Unit, and measure them with
-                    // intermediate constraints using the measurePolicy we created above.
-                    with(myMeasurePolicy) {
-                        measure(
-                            measurablesForSlot(Unit),
-                            intermediateConstraints
-                        )
-                    }
-                },
-                measurePolicy = { constraints ->
-                    val measurables = subcompose(Unit) { content() }
-                    with(myMeasurePolicy) { measure(measurables, constraints) }
-                })
-        }
-    }
-}
\ No newline at end of file
+enum class SlotsEnum { Main, Dependent }
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
index 378d5b7..942c141 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
@@ -196,7 +196,7 @@
                         Box(Modifier.fillMaxSize())
                     }[0].measure(constraints)
                     val size = placeable.run { IntSize(width, height) }
-                    if (this is SubcomposeIntermediateMeasureScope) {
+                    if (!isLookingAhead) {
                         actualSize = size
                     } else {
                         actualTargetSize = size
@@ -589,30 +589,12 @@
 
     @Test
     fun defaultMeasurePolicyInSubcomposeLayout() {
-        var actualLookaheadSize by mutableStateOf(IntSize.Zero)
         var defaultIntermediateMeasureSize by mutableStateOf(IntSize.Zero)
         rule.setContent {
             CompositionLocalProvider(LocalDensity provides Density(1f)) {
                 LookaheadScope {
                     SubcomposeLayout(
                         Modifier
-                            .fillMaxSize()
-                            .requiredSize(200.dp),
-                        intermediateMeasurePolicy = { constraints ->
-                            measurablesForSlot(Unit)[0].measure(constraints)
-                            actualLookaheadSize = this.lookaheadSize
-                            layout(0, 0) {}
-                        }
-                    ) { constraints ->
-                        val placeable = subcompose(Unit) {
-                            Box(Modifier.requiredSize(400.dp, 600.dp))
-                        }[0].measure(constraints)
-                        layout(500, 300) {
-                            placeable.place(0, 0)
-                        }
-                    }
-                    SubcomposeLayout(
-                        Modifier
                             .size(150.dp)
                             .intermediateLayout { measurable, _ ->
                                 measurable
@@ -636,7 +618,6 @@
             }
         }
         rule.runOnIdle {
-            assertEquals(IntSize(500, 300), actualLookaheadSize)
             assertEquals(IntSize(500, 300), defaultIntermediateMeasureSize)
         }
     }
@@ -1719,12 +1700,18 @@
             mutableStateListOf<Int>().apply { addAll(expectedPlacementOrder1) }
 
         var iteration by mutableStateOf(0)
+        var lookaheadConstraints by mutableStateOf<Constraints?>(null)
         // Expect the default placement to be the same as lookahead
         rule.setContent {
             LookaheadScope {
-                SubcomposeLayout(
-                    intermediateMeasurePolicy = { lookaheadMeasurePolicy(lookaheadConstraints) }
-                ) { constraints ->
+                SubcomposeLayout { incomingConstraints ->
+                    val constraints = if (isLookingAhead) {
+                        lookaheadConstraints = incomingConstraints
+                        incomingConstraints
+                    } else {
+                        lookaheadConstraints!!
+                    }
+
                     val placeables = mutableListOf<Placeable>()
                     repeat(3) { id ->
                         subcompose(id) {
@@ -1975,20 +1962,32 @@
                         }
                         SubcomposeLayout(
                             Modifier
-                                .fillMaxSize()
-                                .requiredSize(200.dp),
-                            intermediateMeasurePolicy = { constraints ->
-                                assertFalse(isLookingAhead)
-                                measurablesForSlot(Unit)[0].measure(constraints)
-                                layout(0, 0) {}
-                            }
+                                .layout { measurable, constraints ->
+                                    measurable.measure(constraints).run {
+                                        if (isLookingAhead) {
+                                            assertEquals(500, width)
+                                            assertEquals(300, height)
+                                        } else {
+                                            assertEquals(100, width)
+                                            assertEquals(120, height)
+                                        }
+                                        layout(width, height) {
+                                            place(0, 0)
+                                        }
+                                    }
+                                }
                         ) { constraints ->
-                            assertTrue(isLookingAhead)
                             val placeable = subcompose(Unit) {
                                 Box(Modifier.requiredSize(400.dp, 600.dp))
                             }[0].measure(constraints)
-                            layout(500, 300) {
-                                placeable.place(0, 0)
+                            if (isLookingAhead) {
+                                layout(500, 300) {
+                                    placeable.place(0, 0)
+                                }
+                            } else {
+                                layout(100, 120) {
+                                    placeable.place(0, 0)
+                                }
                             }
                         }
                     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index d95d81b..619812e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -44,7 +44,6 @@
 import androidx.compose.ui.node.requireOwner
 import androidx.compose.ui.platform.createSubcomposition
 import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
 
 /**
@@ -83,155 +82,6 @@
  * for example to use the values calculated during the measurement as params for the composition
  * of the children.
  *
- * When in a [LookaheadScope], [SubcomposeLayout] will be measured up to twice per frame.
- * The two measurements will be using different measure policies and potentially different
- * constraints.
- *
- * The first measurement happens in the lookahead pass, where new layout is calculated based on
- * the target constraints. Therefore, [measurePolicy] will receive the target constraints, and
- * subcompose its content based on the target constraints. Note: Target constraints refers to
- * the constraints that the [SubcomposeLayout] will receive once all the lookahead-based
- * animations on size/constraints in the ancestor layouts have finished.
- *
- * The second measurement is done in the intermediate measure pass after the lookahead pass.
- * The intermediate measure pass allows adjustments to the measurement/placement using the
- * pre-calculated layout information as animation targets to smooth over any
- * any layout changes. In this measurement, [intermediateMeasurePolicy] will be invoked with
- * the intermediate/animating constraints. By default, [measurePolicy] will be invoked in
- * [intermediateMeasurePolicy], and hence the same measure logic in [measurePolicy] with
- * intermediate constraints will be used to measure and layout children in the intermediate
- * pass.
- *
- * Note: When [measurePolicy] is invoked in the intermediate pass, `subcompose` will simply
- * return the measurables associated with the given slot id based on the subcomposition during
- * lookahead pass. This means if a given slot id has not been subcomposed in the lookahead pass,
- * invoking subcompose during intermediate pass will result in an empty list.
- *
- * Possible use cases:
- * * You need to know the constraints passed by the parent during the composition and can't solve
- * your use case with just custom [Layout] or [LayoutModifier].
- * See [androidx.compose.foundation.layout.BoxWithConstraints].
- * * You want to use the size of one child during the composition of the second child.
- * * You want to compose your items lazily based on the available size. For example you have a
- * list of 100 items and instead of composing all of them you only compose the ones which are
- * currently visible(say 5 of them) and compose next items when the component is scrolled.
- *
- * @sample androidx.compose.ui.samples.SubcomposeLayoutWithIntermediateMeasurePolicySample
- *
- * @param modifier [Modifier] to apply for the layout.
- * @param intermediateMeasurePolicy A measure policy that will be invoked during the intermediate
- *                                  measure pass.
- * @param measurePolicy Measure policy which provides ability to subcompose during the measuring.
- */
-@ExperimentalComposeUiApi
-@Composable
-fun SubcomposeLayout(
-    modifier: Modifier = Modifier,
-    intermediateMeasurePolicy:
-    (SubcomposeIntermediateMeasureScope.(Constraints) -> MeasureResult) = { constraints ->
-        lookaheadMeasurePolicy(constraints)
-    },
-    measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult
-) {
-    SubcomposeLayout(
-        state = remember { SubcomposeLayoutState() },
-        modifier = modifier,
-        intermediateMeasurePolicy = intermediateMeasurePolicy,
-        measurePolicy = measurePolicy
-    )
-}
-
-/**
- * [SubcomposeIntermediateMeasureScope] is the receiver scope for the intermediate measurer policy
- * that gets invoked during the intermediate measure pass.
- *
- * When in a [LookaheadScope], [SubcomposeLayout] will be measured up to twice per frame.
- * The two measurements will be using different measure policies and potentially different
- * constraints.
- *
- * The first measurement happens in the lookahead pass, where new layout is calculated based on
- * the target constraints. Therefore, measurePolicy will receive the target constraints, and
- * subcompose its content based on the target constraints. Note: Target constraints refers to
- * the constraints that the [SubcomposeLayout] will receive once all the lookahead-based
- * animations on size/constraints in the ancestor layouts have finished.
- *
- * The second measurement is done in the intermediate measure pass after the lookahead pass.
- * The intermediate measure pass allows adjustments to the measurement/placement using the
- * pre-calculated layout information as animation targets to smooth over any
- * any layout changes. In this measurement, intermediateMeasurePolicy will be invoked with
- * the intermediate/animating constraints. By default, measure policy will be invoked in
- * intermediateMeasurePolicy, and hence the same measure logic in measurePolicy with
- * intermediate constraints will be used to measure and layout children in the intermediate
- * pass.
- *
- * Note: When measurePolicy is invoked in [SubcomposeIntermediateMeasureScope], `subcompose` will
- * simply retrieve the measurables associated with the given slot id based on the subcomposition
- * during lookahead pass. This means if a given slot id has not been subcomposed in the lookahead
- * pass, invoking subcompose during intermediate pass will result in an empty list.
- *
- * @sample androidx.compose.ui.samples.SubcomposeLayoutWithIntermediateMeasurePolicySample
- */
-@ExperimentalComposeUiApi
-sealed interface SubcomposeIntermediateMeasureScope : SubcomposeMeasureScope {
-    /**
-     * Returns the list of measureables associated with [slotId] that was subcomposed in the
-     * [SubcomposeLayout]'s measurePolicy block during the lookahead pass. If the given [slotId]
-     * was not used in the subcomoposition, the returned list will be empty.
-     */
-    fun measurablesForSlot(slotId: Any?): List<Measurable>
-
-    /**
-     * The size returned in the [MeasureResult] by the measurePolicy invoked during lookahead pass.
-     */
-    val lookaheadSize: IntSize
-
-    /**
-     * This is the measure policy that is supplied to SubcomposeLayout in the measurePolicy
-     * parameter. It is used in the lookahead pass, and it is also invoked by default in the
-     * intermediateMeasurePolicy for the intermediate measure pass.
-     *
-     * During the intermediate pass, the [lookaheadMeasurePolicy] will receive potentially
-     * different (i.e. animating) constraints, and will subsequently remeasure and replace
-     * all children according to the new constraints.
-     *
-     * Note: Intermediate measure pass will NOT run **new** subcompositions. [subcompose]
-     * calls in from the [lookaheadMeasurePolicy] in this pass will instead retrieve the measurables
-     * for the given slot based on the subcomposition from lookahead pass. In the rare
-     * case where slots are subcomposed conditionally dependent on constraints, it's recommended
-     * to provide to [SubcomposeLayout] a custom intermediate measure policy. A less desirable
-     * solution to this use case is to invoke [lookaheadMeasurePolicy] with [lookaheadConstraints]
-     * as its parameter, which will skip any intermediate stages straight to the lookahead
-     * sizes & positions.
-     */
-    val lookaheadMeasurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult
-
-    /**
-     * Returns the [Constraints] used in the lookahead pass.
-     *
-     * Note: Using this with [lookaheadMeasurePolicy] will effectively skip any intermediate stages
-     * from lookahead-based layout animations. Therefore it is recommended to use [Constraints]
-     * passed to intermediate measure policy to measure and layout children during intermediate
-     * pass. The only exception to that should be when some of the subcompositions are conditional.
-     * In that case, a custom intermediate measure policy should ideally be provided to
-     * [SubcomposeLayout]. Using [lookaheadConstraints] with [lookaheadMeasurePolicy] should be
-     * considered as the last resort.
-     */
-    val lookaheadConstraints: Constraints
-
-    /**
-     * This function retrieves [Measurable]s created for [slotId] based on
-     * the subcomposition that happened in the lookahead pass. If [slotId] was not subcomposed
-     * in the lookahead pass, [subcompose] will return an [emptyList].
-     */
-    override fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable> =
-        measurablesForSlot(slotId)
-}
-
-/**
- * Analogue of [Layout] which allows to subcompose the actual content during the measuring stage
- * for example to use the values calculated during the measurement as params for the composition
- * of the children.
- *
  * Possible use cases:
  * * You need to know the constraints passed by the parent during the composition and can't solve
  * your use case with just custom [Layout] or [LayoutModifier].
@@ -254,66 +104,6 @@
     modifier: Modifier = Modifier,
     measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult
 ) {
-    @OptIn(ExperimentalComposeUiApi::class)
-    SubcomposeLayout(state, modifier, { lookaheadMeasurePolicy(it) }, measurePolicy)
-}
-
-/**
- * Analogue of [Layout] which allows to subcompose the actual content during the measuring stage
- * for example to use the values calculated during the measurement as params for the composition
- * of the children.
- *
- * When in a [LookaheadScope], [SubcomposeLayout] will be measured up to twice per frame.
- * The two measurements will be using different measure policies and potentially different
- * constraints.
- *
- * The first measurement happens in the lookahead pass, where new layout is calculated based on
- * the target constraints. Therefore, [measurePolicy] will receive the target constraints, and
- * subcompose its content based on the target constraints. Note: Target constraints refers to
- * the constraints that the [SubcomposeLayout] will receive once all the lookahead-based
- * animations on size/constraints in the ancestor layouts have finished.
- *
- * The second measurement is done in the intermediate measure pass after the lookahead pass.
- * The intermediate measure pass allows adjustments to the measurement/placement using the
- * pre-calculated layout information as animation targets to smooth over any
- * any layout changes. In this measurement, [intermediateMeasurePolicy] will be invoked with
- * the intermediate/animating constraints. By default, [measurePolicy] will be invoked in
- * [intermediateMeasurePolicy], and hence the same measure logic in [measurePolicy] with
- * intermediate constraints will be used to measure and layout children in the intermediate
- * pass.
- *
- * Note: When [measurePolicy] is invoked in the intermediate pass, `subcompose` will simply
- * return the measurables associated with the given slot id based on the subcomposition during
- * lookahead pass. This means if a given slot id has not been subcomposed in the lookahead pass,
- * invoking subcompose during intermediate pass will result in an empty list.
- *
- * Possible use cases:
- * * You need to know the constraints passed by the parent during the composition and can't solve
- * your use case with just custom [Layout] or [LayoutModifier].
- * See [androidx.compose.foundation.layout.BoxWithConstraints].
- * * You want to use the size of one child during the composition of the second child.
- * * You want to compose your items lazily based on the available size. For example you have a
- * list of 100 items and instead of composing all of them you only compose the ones which are
- * currently visible(say 5 of them) and compose next items when the component is scrolled.
- *
- * @param state the state object to be used by the layout.
- * @param modifier [Modifier] to apply for the layout.
- * @param intermediateMeasurePolicy A measure policy that will be invoked during the intermediate
- *                                  measure pass.
- * @param measurePolicy Measure policy which provides ability to subcompose during the measuring.
- */
-@Composable
-@UiComposable
-@ExperimentalComposeUiApi
-fun SubcomposeLayout(
-    state: SubcomposeLayoutState,
-    modifier: Modifier = Modifier,
-    intermediateMeasurePolicy:
-    (SubcomposeIntermediateMeasureScope.(Constraints) -> MeasureResult) = { constraints ->
-        lookaheadMeasurePolicy(constraints)
-    },
-    measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult
-) {
     val compositionContext = rememberCompositionContext()
     val materialized = currentComposer.materialize(modifier)
     val localMap = currentComposer.currentCompositionLocalMap
@@ -323,10 +113,6 @@
             set(state, state.setRoot)
             set(compositionContext, state.setCompositionContext)
             set(measurePolicy, state.setMeasurePolicy)
-            set(
-                intermediateMeasurePolicy,
-                state.setIntermediateMeasurePolicy
-            )
             set(localMap, ComposeUiNode.SetResolvedCompositionLocals)
             set(materialized, ComposeUiNode.SetModifier)
         }
@@ -359,6 +145,11 @@
      * used during the previous measuring.
      * @param content the composable content which defines the slot. It could emit multiple
      * layouts, in this case the returned list of [Measurable]s will have multiple elements.
+     * **Note:** When a [SubcomposeLayout] is in a [LookaheadScope], the subcomposition only
+     * happens during the lookahead pass. In the post-lookahead/main pass, [subcompose] will
+     * return the list of [Measurable]s that were subcomposed during the lookahead pass. If the
+     * structure of the subtree emitted from [content] is dependent on incoming constraints,
+     * consider using constraints received from the lookahead pass for both passes.
      */
     fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable>
 }
@@ -368,7 +159,6 @@
  *
  * [slotReusePolicy] the policy defining what slots should be retained to be reused later.
  */
-@OptIn(ExperimentalComposeUiApi::class)
 class SubcomposeLayoutState(
     private val slotReusePolicy: SubcomposeSlotReusePolicy
 ) {
@@ -396,16 +186,6 @@
         SubcomposeSlotReusePolicy(maxSlotsToRetainForReuse)
     )
 
-    /**
-     * Returns whether the [SubcomposeLayout] is in a [LookaheadScope]. Intermediate measure policy
-     * will be only invoked when in a [LookaheadScope].
-     */
-    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
-    @get:ExperimentalComposeUiApi
-    @ExperimentalComposeUiApi
-    val isInLookaheadScope: Boolean
-        get() = state.isInLookaheadScope
-
     private var _state: LayoutNodeSubcompositionsState? = null
     private val state: LayoutNodeSubcompositionsState
         get() = requireNotNull(_state) {
@@ -428,10 +208,6 @@
         LayoutNode.((SubcomposeMeasureScope.(Constraints) -> MeasureResult)) -> Unit =
         { measurePolicy = state.createMeasurePolicy(it) }
 
-    internal val setIntermediateMeasurePolicy:
-        LayoutNode.(SubcomposeIntermediateMeasureScope.(Constraints) -> MeasureResult) -> Unit =
-        { state.intermediateMeasurePolicy = it }
-
     /**
      * Composes the content for the given [slotId]. This makes the next scope.subcompose(slotId)
      * call during the measure pass faster as the content is already composed.
@@ -601,18 +377,8 @@
     // this map contains active slotIds (without precomposed or reusable nodes)
     private val slotIdToNode = mutableMapOf<Any?, LayoutNode>()
     private val scope = Scope()
-    private val intermediateMeasureScope = IntermediateMeasureScopeImpl()
+    private val postLookaheadMeasureScope = PostLookaheadMeasureScopeImpl()
 
-    /**
-     * This is the intermediateMeasurePolicy that developers set in [SubcomposeLayout]. It defaults
-     * to invoking [SubcomposeIntermediateMeasureScope.lookaheadMeasurePolicy].
-     *
-     * Note: This intermediate measure policy is only invoked when in a [LookaheadScope].
-     */
-    internal var intermediateMeasurePolicy:
-        (SubcomposeIntermediateMeasureScope.(Constraints) -> MeasureResult) = {
-        lookaheadMeasurePolicy(it)
-    }
     private val precomposeMap = mutableMapOf<Any?, LayoutNode>()
     private val reusableSlotIdsSet = SubcomposeSlotReusePolicy.SlotIdsSet()
 
@@ -840,7 +606,6 @@
     fun createMeasurePolicy(
         block: SubcomposeMeasureScope.(Constraints) -> MeasureResult
     ): MeasurePolicy {
-        intermediateMeasureScope.lookaheadMeasurePolicy = block
         return object : LayoutNode.NoIntrinsicsMeasurePolicy(error = NoIntrinsicsMessage) {
             override fun MeasureScope.measure(
                 measurables: List<Measurable>,
@@ -849,17 +614,14 @@
                 scope.layoutDirection = layoutDirection
                 scope.density = density
                 scope.fontScale = fontScale
-                val isIntermediate =
-                    (root.layoutState == LayoutState.Measuring ||
-                        root.layoutState == LayoutState.LayingOut) && root.lookaheadRoot != null
-                if (isIntermediate) {
-                    return intermediateMeasurePolicy.invoke(intermediateMeasureScope, constraints)
+                if (!isLookingAhead && root.lookaheadRoot != null) {
+                    return with(postLookaheadMeasureScope) {
+                        block(constraints)
+                    }
                 } else {
                     currentIndex = 0
-                    intermediateMeasureScope.lookaheadConstraints = constraints
                     val result = scope.block(constraints)
                     val indexAfterMeasure = currentIndex
-                    intermediateMeasureScope.lookaheadSize = IntSize(result.width, result.height)
                     return object : MeasureResult {
                         override val width: Int
                             get() = result.width
@@ -1008,19 +770,15 @@
             [email protected](slotId, content)
     }
 
-    private inner class IntermediateMeasureScopeImpl :
-        SubcomposeIntermediateMeasureScope, MeasureScope by scope {
-        override fun measurablesForSlot(slotId: Any?): List<Measurable> =
-            slotIdToNode[slotId]?.childMeasurables ?: emptyList()
-
+    private inner class PostLookaheadMeasureScopeImpl :
+        SubcomposeMeasureScope, MeasureScope by scope {
         /**
-         * This is the size returned in the MeasureResult in the measure policy from the lookahead
-         * pass.
+         * This function retrieves [Measurable]s created for [slotId] based on
+         * the subcomposition that happened in the lookahead pass. If [slotId] was not subcomposed
+         * in the lookahead pass, [subcompose] will return an [emptyList].
          */
-        override var lookaheadSize: IntSize = IntSize.Zero
-        override lateinit var lookaheadMeasurePolicy:
-            SubcomposeMeasureScope.(Constraints) -> MeasureResult
-        override var lookaheadConstraints: Constraints = Constraints()
+        override fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable> =
+            slotIdToNode[slotId]?.childMeasurables ?: emptyList()
     }
 }
 
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 8ce18e2..b2b8c41 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -214,29 +214,30 @@
     docs("androidx.media2:media2-session:1.2.1")
     docs("androidx.media2:media2-widget:1.2.1")
     docs("androidx.media:media:1.6.0")
-    docs("androidx.media3:media3-cast:1.0.1")
-    docs("androidx.media3:media3-common:1.0.1")
-    docs("androidx.media3:media3-database:1.0.1")
-    docs("androidx.media3:media3-datasource:1.0.1")
-    docs("androidx.media3:media3-datasource-cronet:1.0.1")
-    docs("androidx.media3:media3-datasource-okhttp:1.0.1")
-    docs("androidx.media3:media3-datasource-rtmp:1.0.1")
-    docs("androidx.media3:media3-decoder:1.0.1")
-    docs("androidx.media3:media3-effect:1.0.1")
-    docs("androidx.media3:media3-exoplayer:1.0.1")
-    docs("androidx.media3:media3-exoplayer-dash:1.0.1")
-    docs("androidx.media3:media3-exoplayer-hls:1.0.1")
-    docs("androidx.media3:media3-exoplayer-ima:1.0.1")
-    docs("androidx.media3:media3-exoplayer-rtsp:1.0.1")
-    docs("androidx.media3:media3-exoplayer-smoothstreaming:1.0.1")
-    docs("androidx.media3:media3-exoplayer-workmanager:1.0.1")
-    docs("androidx.media3:media3-extractor:1.0.1")
-    docs("androidx.media3:media3-session:1.0.1")
-    docs("androidx.media3:media3-test-utils:1.0.1")
-    docs("androidx.media3:media3-test-utils-robolectric:1.0.1")
-    docs("androidx.media3:media3-transformer:1.0.1")
-    docs("androidx.media3:media3-ui:1.0.1")
-    docs("androidx.media3:media3-ui-leanback:1.0.1")
+    docs("androidx.media3:media3-cast:1.1.0-alpha01")
+    docs("androidx.media3:media3-common:1.1.0-alpha01")
+    docs("androidx.media3:media3-database:1.1.0-alpha01")
+    docs("androidx.media3:media3-datasource:1.1.0-alpha01")
+    docs("androidx.media3:media3-datasource-cronet:1.1.0-alpha01")
+    docs("androidx.media3:media3-datasource-okhttp:1.1.0-alpha01")
+    docs("androidx.media3:media3-datasource-rtmp:1.1.0-alpha01")
+    docs("androidx.media3:media3-decoder:1.1.0-alpha01")
+    docs("androidx.media3:media3-effect:1.1.0-alpha01")
+    docs("androidx.media3:media3-exoplayer:1.1.0-alpha01")
+    docs("androidx.media3:media3-exoplayer-dash:1.1.0-alpha01")
+    docs("androidx.media3:media3-exoplayer-hls:1.1.0-alpha01")
+    docs("androidx.media3:media3-exoplayer-ima:1.1.0-alpha01")
+    docs("androidx.media3:media3-exoplayer-rtsp:1.1.0-alpha01")
+    docs("androidx.media3:media3-exoplayer-smoothstreaming:1.1.0-alpha01")
+    docs("androidx.media3:media3-exoplayer-workmanager:1.1.0-alpha01")
+    docs("androidx.media3:media3-extractor:1.1.0-alpha01")
+    docs("androidx.media3:media3-muxer:1.1.0-alpha01")
+    docs("androidx.media3:media3-session:1.1.0-alpha01")
+    docs("androidx.media3:media3-test-utils:1.1.0-alpha01")
+    docs("androidx.media3:media3-test-utils-robolectric:1.1.0-alpha01")
+    docs("androidx.media3:media3-transformer:1.1.0-alpha01")
+    docs("androidx.media3:media3-ui:1.1.0-alpha01")
+    docs("androidx.media3:media3-ui-leanback:1.1.0-alpha01")
     docs("androidx.mediarouter:mediarouter:1.6.0-alpha03")
     docs("androidx.mediarouter:mediarouter-testing:1.6.0-alpha03")
     docs("androidx.metrics:metrics-performance:1.0.0-alpha04")
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index a595aaf..9d82f9b 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -3838,6 +3838,64 @@
 =hALf
 -----END PGP PUBLIC KEY BLOCK-----
 
+pub    3D12CA2AC19F3181
+uid    Tatu Saloranta (cowtowncoder) <[email protected]>
+
+sub    575D6C921D84AC76
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQINBGL4BxIBEAC+lX44fd/zrVQPzdKygarBd/X0bBpGakT++Kfk4UBGl3q+wd2G
+R9puB9R377ds8hU7U3To8sHguUZo6DbD9Gb/is/WajSb9g92z+rMow3KbqfCYqWr
+kaIj27OJgbziFcnMAtvGoFRfaPI/7TOwEw3jT7B87RXeiATX4iL8fzMUmkfZm0Hk
+qjnepMQeaz3KzMY4DfBcI45kwzl3EIBFIlk428mhBU5iAAANoyPsimfqEPRCUDjx
+vT8g7PvpkBdNZgRS6R9vLxyzKi/f5KswZIMvop/pRXIhAKDhCCyr2GD+T3JoIKp9
+kvS1MQucWeX8+TFWh5qEA3e06Xu0JSdPCEej0BH06EiTMsAOU5bWqgLAO9DVpS32
+I092KAuMJlEPCnz7IGXVkeNY5KYrlsmoKrBO3GF/zsCyiZDvSULkVJcrtBCYOrgq
+HRIzvJWQaTJ5V15MD8CZIELyjCGZ8Jy8hdZpaTjYalw0bUq+yRAqMD5slp6A1tnv
+jyqVTgU+yRGq2HB90vJ0D3P1w4xRDuNF8c02futO415Yc/qkyh3/5AjGSoocrlfX
+cMreJXpQWVsvXn3NsitjsA6XOJpMOgipCDxfvn8SSLl9fWNJf55j7fCkBokF/lIi
+81RVQbyjVCOV0OEqHJLP9asPHyAFvUppNWtcvViPxVmb52djnw/x/61WVQARAQAB
+tDVUYXR1IFNhbG9yYW50YSAoY293dG93bmNvZGVyKSA8dGF0dS5zYWxvcmFudGFA
+aWtpLmZpPokCVAQTAQgAPhYhBCgRjAcMsioBdaLo1D0SyirBnzGBBQJi+AcSAhsD
+BQkJZgGABQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJED0SyirBnzGBnxsQALTJ
+d/chCW8zWgR6x9ZDfU2f5fgMhi8jjILCieiQdQ/vec3QqCVLpJmE+l6MrI75E7GY
+eL8Iu0DXO26nHHSEbDa9kGk+ohdODbOd+y6KE5t6qPFaHSG5Gj1iiQ7libmrO7EZ
+qPQzl4fwmzUvl/2x+kaL5WsC4SxbwTG2mGo8WRCz+hqM40yeBeKYxpTlN2VBrlU2
+xkJ1I0rQITtHwck1dClgf4up13uBna7TlCBJc7vio3OwAFbXTPHEL+efrn8zk0au
+J+cwocHmm4d9shGiwiSUAOEnQ8FZ08IwO5MoOUWKz/ARYLuR6FAd8HqFERbfUxU2
+AlcyLSAhSi0c+j2Gi9PFkNTABEDHpdTosaAgKpbYdGke/2sfA0BKmcNRIfGjs+W0
+GnX2c7tP6qc7iFCCP0TjXvR8jnCmH8BT1JiSN5C/JPH0hDE1Zvmvgs0hid48e6Zv
+k1yXvEdtd/rH0uM3yUs28EfdcqokhwKZi5duUeizgG1gGNCW8RGvhi5ks1bABK9i
+52JMZznINMb35mZ0bEK7YLi3K8DE2hfIU6yg8WnlXlx3eOK4e6jWxWbjuEmUJ7BE
+ZWvz5IJ9nFI1UAIq7Ug7ruM+CH8ntpHueuGp6goq39+0lTd/1Sc29kVadg/DXQtC
+mDDrw4Btpz8hXthmTnNVvP/GUu5K1fttW3gNU/ofuQINBGL4BxIBEACwpcarOeEN
+5TztQDTvemc+DXZuWirmHv1TyJmjOhU0hGGMzEnKKU6VZlUIg5YQNYknUOfOf/05
+pgQmsEhjjI8NBD3Cys6SVQ6wOlkA+KpDKs/dXwyJttYE+EG/IMzjwZW2DbF50Hkm
+T2VK/oFhRSf0Par+cbVQ/mNLAVC7ueZWBVXldezqVvk3tDYYZef9T8Qlf999LVXF
+giMzRFrzLLcd2KEHAX4se65FTxIfFYYCrshFIKDRi4IWNzQqtPV7mb94wXY0Vwse
+5mMEgjmieGPjBGYne2JU0xYNBxtly1y3aeDXcxNlNrcS/Ake9AqWAYU4agtocCef
+b1pt5Q3li0qg3PsVKDJ5qWDceb+kgcUuHgtwHFCVICoQUMsv9p5F/kWL38/OWcTR
+2lQ4tGerE2dmlyqFWu7mpELckAfXSpJobZltUbp8CO679g0lk+OJWSmxddlVybdX
+CFaAeOKQb0woQOkR1vo2tJHyGmGr20Eea+UX+kdLojVQwYCqICdvnK4YpHuhpT5c
+rzk6lohfZpBPMHdpR7FQQZeQEW5EbcFNfoUEwsgb2qkG+hQIL5Q7wRajHkGaG3Wu
+hY2xEyoHuLSb20hP5hI2uhtxswUl/+IOjjZtDCjzLz59Q+ADkDZYM+PN3eR0UJDq
+YKly49KTuz23zLluNjNwqo8K0y05XGCQMwARAQABiQI8BBgBCAAmFiEEKBGMBwyy
+KgF1oujUPRLKKsGfMYEFAmL4BxICGwwFCQlmAYAACgkQPRLKKsGfMYGQbQ//bxZq
+IUIrPa86oXELq19E+OFTvCKVQzgohiaKPS6Hx2rgtvPS2bJvO+rXlAvRAFyI8sX7
+Dq0deeZP0pXefidjpfjKz1bhW8Wf7RU6QOKc0Eyl8/YqCVZign1DzJlF1r2mrkZa
+0VRNLjh86P/Y1T7ZhrAiR0PNv++LEgNsPbL8Tu1ryWl8vWFdlos40W8xleP1nBU1
+9OWheOPU9VYN80eBD5ij2Cn0LCxJQdyNOpVD03P3Ycauk9OHOVCd+UVX2A7VwemX
+b4wRDL59gAfSGyYLHYqAF1XJbOXkheTPRsttxXRIkzvA/gjpmQmioNU3UhiMZ1EJ
+kbJ42loFPv7YplbmBXoMAKJF1402+sOVusC1FULMQQtpZvC/bgobqEqdTzhZa/Hr
+KA45BFpcaTO80jJto8kiZR7infwX1gBGrYgwXisxiiYPI5yVwAvYTcNnU1nD9Vk5
+iyTgx0BNBG1hLPaZBtkYarDmUo0KyYf0Y8mQZmd7U0nOHptdkR+5yND5yO29/wGf
+se1KeZcRNGgcYQFKGw72HDiYsOELiiwsFqyUMWPLEwQJave/tO4SYSY1wP4rA3IA
+IU24GMQFZ64lljAKQCRXLwDKK2tMyHz8I+GT+0+bbpz4ojkEmxCDxXfgUy1362tG
+FdEbaZ88HZxTa30iXtOXOLvWIYRp8hv3pSb8id8=
+=hALf
+-----END PGP PUBLIC KEY BLOCK-----
+
 pub    CC6346F2CE3872D9
 uid    Jisi Liu <[email protected]>
 
@@ -12161,6 +12219,41 @@
 =S4Fa
 -----END PGP PUBLIC KEY BLOCK-----
 
+pub    26E74B6874AEE127
+uid    Sam Judd <[email protected]>
+
+sub    B4E75C15C3C701AE
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQENBFPsGJIBCADOxQoTLxpZVRIbLaRfsHa2y/TEIGvxLP7TgqTwspZYnwBd0cOW
+OHAvF8yGfdk5gvkGTlQ/xchwu2Ix05FO2c+fBoOgIG1Gn2Q+PwheZklS7S+V+GFk
+t5S3m6b54Pl2lKbcasaM99r5g2+MhxwVnG66ldLS9fdPWqWrviOC+EipHfGyuhj0
+R0E2Oh+dNHrsZR0vtoJawA8xDP5D1Ii3jEVdi71PF8OlavG7saAZXtOW1kU77oPa
+SmGFTmcFLjDP1alo95TTfBihaYiwFFnW7NQEi5Bw1u3G0+Vaoo5eJBQwYsNxLs3h
+I1d7wsqFW5LK6LSxEjfvEhJypi0XzrFkFzMvABEBAAG0H1NhbSBKdWRkIDxzYW0u
+YS5qdWRkQGdtYWlsLmNvbT6JATgEEwECACIFAlPsGJICGwMGCwkIBwMCBhUIAgkK
+CwQWAgMBAh4BAheAAAoJECbnS2h0ruEn9DUIAItn8TzZh44JQ0aRO3dgmh4kUnnJ
+cJyw+0m5m4gGyjZ3vFzbCXgzCHAz2jN3bh6crnUoheIUCn206NRz+aG0JGXAkgV7
+9R7QAKY3K8QzW0QWqHOKv1n4FbfPKhaGMnXYUcQjV9pkVgDmqV4QzjmegATVLkOE
+VVNUjWIP6wXwj0I/UW4jO608tTi9juH+XkDkpf+qqix8FUn81tw7/weWuZXFTpAe
+qAYzjtzzDse8uI1tZWxc8H6AvKCuigoTsD6xkfuPfL4a3JepER0R3WlGhW9hDLBc
+/VXGULLl612XfeG+xREy/pjILzp4ajTC9FdAmdWF2wQWhM9ZXt38FSBHg4+5AQ0E
+U+wYkgEIALu35o9Q309zBBZD6WLTMvFt/X+N6S/cUrNLMv3YvR/Bo1mtDxsYwmzo
+dWOHo6TNthWm1FIkDhHUMB58q9oHzL2/JIL2k/jCULOhj+YAT2N5fQwvZ2WBtWtf
+suFYFO5y7UKKUqo+2Dhnk8YuorPbKjVXELrHFIq0IRlKEKnDfNrkxwHBpfeU4QaC
+UcCW+OjRZFsgZVVc8ZjhvvKd9/vX4Jt/c4/O52Ym+YEYtZaiawcNYFAdMhaiwSF+
+dof+v2e6BIYOJrBifZuILqA87XsHQyBCABO1zhspt5X2QGGqkjVHiFJnYiTwqYX7
+2n1OpoMOklJZ+ShbbGpCst81i7lyz28AEQEAAYkBHwQYAQIACQUCU+wYkgIbDAAK
+CRAm50todK7hJ8OJCACtdgGnPNmYILYJ3jyXh+WPDvxr3y3yMKmC0pRhgs/WxGK6
+cmRV/ej8OB5aIWCaoWeJ03twgqiaSMbn+7ONkr1DbPLzqQGY+QJBJEvLE9m9YcAd
+BMHCT0fn9Op0AYQhoInOPcKLD6NFO+c91uLBOg0q316Wlpl0p3ULH526a2YVPeKw
+mjCVlhrQSoHgxagNrol4gC8bfpypsjTwCccjesiYMOPPU6eyHelFAAGEuVDmkh9g
+Gb9wOqgum0vJEummqOStTopEGr63PoWVMXSQbkrTrayCcxjSiKRfK10myhIGFU2n
+pCL9QbIT7Aw2vZ5VjIm6fWIOPJJz4dsRuYL9Ar8c
+=es9L
+-----END PGP PUBLIC KEY BLOCK-----
+
 pub    82B5574242C20D6F
 -----BEGIN PGP PUBLIC KEY BLOCK-----
 Version: BCPG v1.68
@@ -14226,6 +14319,64 @@
 =TnZo
 -----END PGP PUBLIC KEY BLOCK-----
 
+pub    5A68A2249128E2C6
+uid    Tink Developers (Signing key for Maven artifacts) <[email protected]>
+
+sub    4E5C59DBFF7DACF9
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQINBGPcwwABEADTw/gqmHh4LTSDsBP0KMoXFtFQnv7xmVPPrPjt0NxGn3w2WIou
+7UaLUTViKkgm92h72gyM7N9JfNBLcYrqVf9ed75MPdGQgzIhkVg3SLWZGFoIQUJ4
+VznKuqJmd0dSRtApXL9ZoVXf1mLnbLkOvfLfw2hVIsMJcW9/G4It7pPY82IiwTLn
+XG/pw6+wLa5FGCM4mldPnyBDR935nSkgnZzQJyDESXZKS1uiU1rMcGWkVLJ1UYfg
+fT5c6jAk+75vhyQEvHReoa1T8fgBPD0jAlE7T80460x8dramshhAAIOZLnlAuiBN
+A7KPY7cUDxDyFNLdhj7lGjPP1UTv5mdcZc0H6tgaapOB8QzqnkAJN7GrPHjAWnu7
+ujdiT+lmng+waiBfoQN7HQyJXng8Skj1tVYjuAUNgUuA6p1hL30k9Ny9wO2BBg23
+OXYn8yLptZCUg4T31w2ko2PReSxMeEI6S2jWTALP9HH1Q1sinQnlJ8SfPAssG8wn
+qjaV4PtS8bO+Gy2NosG389dzibrmVJAHqymTLlaviWgeqAXEwZhbVcSOv+B3JgAX
+h1dI2zDJUMGV7jNbKa+UNGb+di8T3J5JEXCNM/Zvm3KNudfZFbcNS1pEzNRzm4gh
+kmNHQEtknSm6NHaWIP5eMIxsKGUA6qTR8XE9qrvpwV35rwbxmPHSowHzzwARAQAB
+tEdUaW5rIERldmVsb3BlcnMgKFNpZ25pbmcga2V5IGZvciBNYXZlbiBhcnRpZmFj
+dHMpIDx0aW5rLWRldkBnb29nbGUuY29tPokCTgQTAQoAOBYhBHOXbJw5wUebhOJk
+GlpooiSRKOLGBQJj3MMAAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEFpo
+oiSRKOLGX3YP/iAVKH2H5ZKxO78FTLS8Jf1bl48Z31kTnvZg/sMFA51D9p+kBxns
+dNe/4UAt7R7hchlJ6tah9+L5bBf3eU0r4vNwE7+d6WiYoKgGVaegRHfOJEqSlnXo
+a6b3J2ML2vgnRpWTFJPdtZXKekodf76We6YshbHfuix5B0Zfztn2UC54cuhCsi83
+FhEMW9WvMe4Men9u4MsiQU+wCYUtsRV6bZpLWbrYFJyB75/IwWiLZv4AiVIqUpB6
+yw/lTDsPTyrncEfE2+DYwyHduieY+gIR6SGkJh+7VF3ESj6UR1i7+sdcvWbLgJXe
+JoqQBROxnApKh/3LIDUGuONfiryEaC5Xy/l1YkEUsgp5RA5YrotFkTEZCvud6M9m
+iBrFOz0xAHwu84OfcSJsmzARh8lItd4wImykXnhyiTInVUrq9OeDN94RSTFQ7+Mh
+RT+CBnh6Z8y3jL9ekCpGZrxB0K4T4FBxQ0QNLnPZK2OfTEvmC2haP2sofbrYbo31
+TlafpzPfLXyp5sBGevT/vUd3YbXS69L3lDRKoOnJy3PK3UUpeSxTP0sN53mnembu
+8R6eHzuisrb1vl9wsIJ0EFn5F331xQagKYzDn0Vp63KltD3xOnB+itcsZETYamuL
+MHk3TXrNg47Iu6fbLEw9M2HHYDBm70AI1PpXtT4mtQ+TU18JtPM4ZpvHuQINBGPc
+wwABEADh/vvuWr2Pl5xD/gF1vKIdy+sNTTadx7EoAsdnrYShtP3jvUPL9VDvbpyH
+K9B6wFEulUM037L7BOl9khg9oO4G/NXlU3wiIJk4dI4tBrj1IAFD7z3qQ2Sgpy/b
+TsBRZZCwOiW28IxqQsx5DE4i5YYOEjjyqZiza4/I3TchKN0sEOwb77MEUrYS1CUe
+lpl/zFlYZNGXT/oDjJI2bVs1pvCMtb2iQW7m6JvDEY0xZ7zoRm2rJA567oV3WAO6
+u2T2tpAzfu1SYJRRPbUt45pdzWSZUzCQwcB0ZTAuQbK4nIsjPGv6oAPm3PgpEpW2
+PBHPux+UHN03k/vEpb5XLLTBuWIdgtXRzD2vSkEO7A6CBkYTY4TY5UodLgV+szYl
+G+N00m0h2SOf/9FEijRIA2XMfNYZ6E8x3I0ADXmOEE1MfdBGSEtk/Tzb/NX8Bb8/
+zk1tKRI00vEz1bjOTsiRZQ6Aod+hUcCPasTUAKIgTpe30zd1v1krF4leVRXHWJbU
+mpsr7CoJRIrKBghkP7K0vLUQzes0djxl/J9C8Ru7bjM6Ndjmy5+oSCMMPJyJ/Wpd
++fEWZxgIukAnjLtuZTUVeMESzP1CkUeNv+aGadPNGw97VRAFEfk6508ihg+TVEiy
+FORV/njYUB+4zm/+aczd6KKT24t6DIVdm1FkSds8SpcDT8Ei0wARAQABiQI2BBgB
+CgAgFiEEc5dsnDnBR5uE4mQaWmiiJJEo4sYFAmPcwwACGwwACgkQWmiiJJEo4sbA
+dRAAlzA7kLCzFnCSYr2TgCfQEoI8yslnPL0flq7ghw5yBK4OdUbYoUBYBroZMJLw
+hPvyaEdp3t63Sl/9GsYNfub+TAOJA64WuRtOT1QbOh6+U5T7X5yvPM3FAGUuYOlk
+1ABuTAtbOWW+iPOpE7sZaai0j9zH2vPyviBqZ6GtvsuVT7ynRbrYuWe9127ZkJet
+6zPzGXoyTE+FaGOdv/wd+9u1Qjk/lYowNoQ7xXWbnF4jD3o6lM56uOgvPUFoSnzb
+sd1fCXcfQ4wj+O3yEoMDVa1K9eIrSz7TrL5K4VzfOxaHxPh2orE8dFgjXy0Vm/KC
+XTOc9DcJXCjqJVh9RoDxTaNkjWfkN+1bq9NUaPauRduMwlkbk904ygXxMJ485hm5
+uSaCKM8eYBp4y/CdwOcnonBEg+lS3rVIcfDUByRrim5pOsIlSe3f2OX3txMYQvXJ
+ivYOA4phKSpntp4TDzGkZfLbCIpaFowR3px/c2LKuQmTmr+Vl8v5W4kBgQGEDdxY
+a7jhYx9HbLimtQG2XcCC+javwdubT/ItHrfcAB6B+dV0iRA5b4QDGtU0CsD9tY3N
+oaTSCeYc7Xae5YCXr3viH8vWPap984XZWLJZXM2s2Vm1XdeBTQxWCGVER9dnzGGE
+DsHNOZoy3UpsL+GQ+AORGsEAjOVJQnCjqUh7gtB8cwiPvps=
+=TnZo
+-----END PGP PUBLIC KEY BLOCK-----
+
 pub    5E1F79A7C298661E
 uid    David P. Baker <[email protected]>
 
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 23ee8db..9a07f43 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -327,6 +327,7 @@
          <trusted-key id="b46dc71e03feeb7f89d1f2491f7a8f87b9d8f501" group="org.jetbrains.trove4j"/>
          <trusted-key id="b47034c19c9b1f3dc3702f8d476634a4694e716a" group="com.googlecode.java-diff-utils"/>
          <trusted-key id="b4ac8cdc141af0ae468d16921da784ccb5c46dd5" group="net.bytebuddy"/>
+         <trusted-key id="b69a63a5ef7183dbc09199ac26e74b6874aee127" group="com.github.bumptech.glide"/>
          <trusted-key id="b6e73d84ea4fcc47166087253faad2cd5ecbb314" group="org.apache.commons"/>
          <trusted-key id="b801e2f8ef035068ec1139cc29579f18fa8fd93b" group="com.google.j2objc"/>
          <trusted-key id="b9cca13c59f21c6ce841a8d1a4b1a03fb9c2ce23" group="com.squareup.leakcanary"/>
@@ -381,6 +382,7 @@
          </trusted-key>
          <trusted-key id="db0597e3144342256bc81e3ec727d053c4481cf5" group="org.tensorflow"/>
          <trusted-key id="dbd744ace7ade6aa50dd591f66b50994442d2d40">
+            <trusting group="com.squareup.okhttp3"/>
             <trusting group="com.squareup.okio"/>
             <trusting group="com.squareup.wire"/>
          </trusted-key>
@@ -477,6 +479,14 @@
             <sha256 value="9fb18fd29b9dfe2e7ed5fe98a3be433a4c3cc4ea8f47f2b444155c39b4afddf5" origin="Generated by Gradle" reason="Unsigned. Used by media3."/>
          </artifact>
       </component>
+      <component group="com.google.ads.interactivemedia.v3" name="interactivemedia" version="3.30.1">
+         <artifact name="interactivemedia-3.30.1.aar">
+            <sha256 value="e2cc79031befb48c6212a0c71cf1535416a8903a9a373ba6c8b95dec95db83c0" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="interactivemedia-3.30.1.pom">
+            <sha256 value="19f7d921a20d3be022620dd78bd708b2c16362cece59781c94b74a51f078243c" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.google.android.apps.common.testing.accessibility.framework" name="accessibility-test-framework" version="2.1">
          <artifact name="accessibility-test-framework-2.1.jar">
             <sha256 value="7b0aa6ed7553597ce0610684a9f7eca8021eee218f2e2f427c04a7fbf5f920bd" origin="Generated by Gradle" reason="Artifact is not signed"/>
@@ -549,6 +559,14 @@
             <sha256 value="c6898b1f71e69b15bf90c31fc3ef2de1cffbf454a770700f755b5a47ea48b540" origin="Generated by Gradle"/>
          </artifact>
       </component>
+      <component group="com.google.android.tv" name="tv-ads" version="1.0.0-alpha02">
+         <artifact name="tv-ads-1.0.0-alpha02.aar">
+            <sha256 value="d92a8eb07b926cdd199f3bdaf33478565cab125b92859e84f7abb5b7219bc5e4" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="tv-ads-1.0.0-alpha02.pom">
+            <sha256 value="7b4315f0127628f2203ab66dfe490a9a07e06a38fb8e15ea2df1a35f1619308d" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.google.camerax.effects" name="portrait" version="0.0.1">
          <artifact name="portrait-0.0.1.aar">
             <sha256 value="86d3e99a6123e3f830bf3f70392b30fc8d238374104ce00a02de20d8613af607" origin="Generated by Gradle"/>
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
index c46fb02..d3332ac 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
@@ -22,6 +22,7 @@
 import androidx.privacysandbox.tools.core.generator.ClientProxyTypeGenerator
 import androidx.privacysandbox.tools.core.generator.CoreLibInfoAndBinderWrapperConverterGenerator
 import androidx.privacysandbox.tools.core.generator.GenerationTarget
+import androidx.privacysandbox.tools.core.generator.SdkActivityLauncherWrapperGenerator
 import androidx.privacysandbox.tools.core.generator.ServerBinderCodeConverter
 import androidx.privacysandbox.tools.core.generator.ServiceFactoryFileGenerator
 import androidx.privacysandbox.tools.core.generator.StubDelegatesGenerator
@@ -29,6 +30,7 @@
 import androidx.privacysandbox.tools.core.generator.TransportCancellationGenerator
 import androidx.privacysandbox.tools.core.generator.ValueConverterFileGenerator
 import androidx.privacysandbox.tools.core.model.ParsedApi
+import androidx.privacysandbox.tools.core.model.containsSdkActivityLauncher
 import androidx.privacysandbox.tools.core.model.getOnlyService
 import androidx.privacysandbox.tools.core.model.hasSuspendFunctions
 import com.google.devtools.ksp.processing.CodeGenerator
@@ -68,6 +70,7 @@
         generateCallbackProxies()
         generateToolMetadata()
         generateSuspendFunctionUtilities()
+        generateSdkActivityLauncherUtilities()
         generateServiceFactoryFile()
     }
 
@@ -151,6 +154,11 @@
             .also(::write)
     }
 
+    private fun generateSdkActivityLauncherUtilities() {
+        if (!api.containsSdkActivityLauncher()) return
+        SdkActivityLauncherWrapperGenerator(basePackageName()).generate().also(::write)
+    }
+
     private fun write(spec: FileSpec) {
         codeGenerator.createNewFile(Dependencies.ALL_FILES, spec.packageName, spec.name)
             .bufferedWriter().use(spec::writeTo)
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt
index c096f86..c23eaad 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/input/com/mysdk/MySdk.kt
@@ -5,6 +5,7 @@
 import androidx.privacysandbox.tools.PrivacySandboxService
 import androidx.privacysandbox.tools.PrivacySandboxValue
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SdkActivityLauncher
 
 @PrivacySandboxService
 interface MySdk {
@@ -31,6 +32,8 @@
     suspend fun returnUiInterface(): MyUiInterface
 
     fun acceptUiInterfaceParam(input: MyUiInterface)
+
+    fun acceptSdkActivityLauncherParam(activityLauncher: SdkActivityLauncher)
 }
 
 @PrivacySandboxInterface
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySdkStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySdkStubDelegate.kt
index 0d5c413..79bd6e5 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySdkStubDelegate.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/MySdkStubDelegate.kt
@@ -1,6 +1,7 @@
 package com.mysdk
 
 import android.content.Context
+import android.os.Bundle
 import androidx.privacysandbox.ui.provider.toCoreLibInfo
 import com.mysdk.PrivacySandboxThrowableParcelConverter
 import com.mysdk.PrivacySandboxThrowableParcelConverter.toThrowableParcel
@@ -172,4 +173,10 @@
       delegate.acceptUiInterfaceParam((input.binder as MyUiInterfaceStubDelegate).delegate)
     }
   }
+
+  public override fun acceptSdkActivityLauncherParam(activityLauncher: Bundle): Unit {
+    coroutineScope.launch {
+      delegate.acceptSdkActivityLauncherParam(SdkActivityLauncherAndBinderWrapper(activityLauncher))
+    }
+  }
 }
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/SdkActivityLauncherAndBinderWrapper.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/SdkActivityLauncherAndBinderWrapper.kt
new file mode 100644
index 0000000..8ef1af5
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/SdkActivityLauncherAndBinderWrapper.kt
@@ -0,0 +1,13 @@
+package com.mysdk
+
+import android.os.Bundle
+import androidx.privacysandbox.ui.core.SdkActivityLauncher
+import androidx.privacysandbox.ui.provider.SdkActivityLauncherFactory
+
+public class SdkActivityLauncherAndBinderWrapper private constructor(
+    private val `delegate`: SdkActivityLauncher,
+    public val launcherInfo: Bundle,
+) : SdkActivityLauncher by delegate {
+    public constructor(launcherInfo: Bundle) :
+            this(SdkActivityLauncherFactory.fromLauncherInfo(launcherInfo), launcherInfo)
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
index 4c4c071..dae0d2ac 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
@@ -26,6 +26,7 @@
 import androidx.privacysandbox.tools.core.model.Types
 import androidx.privacysandbox.tools.core.model.Types.asNullable
 import androidx.privacysandbox.tools.core.model.ValueProperty
+import androidx.privacysandbox.tools.core.model.containsSdkActivityLauncher
 import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
 import androidx.room.compiler.processing.util.Source
 import androidx.testutils.assertThrows
@@ -263,6 +264,59 @@
     }
 
     @Test
+    fun sandboxedUiAdapter_correctlyDetectedInService() {
+        val source = Source.kotlin(
+            "com/mysdk/MySdk.kt", """
+                    import androidx.privacysandbox.tools.PrivacySandboxService
+                    import androidx.privacysandbox.ui.core.SdkActivityLauncher
+                    @PrivacySandboxService
+                    interface MySdk {
+                        fun useLauncher(launcher: SdkActivityLauncher)
+                    }
+                """
+        )
+
+        assertThat(compileAndParseApi(source).containsSdkActivityLauncher()).isTrue()
+    }
+
+    @Test
+    fun sandboxedUiAdapter_correctlyDetectedInInterface() {
+        val source = Source.kotlin(
+            "com/mysdk/MySdk.kt", """
+                    import androidx.privacysandbox.tools.PrivacySandboxService
+                    import androidx.privacysandbox.tools.PrivacySandboxInterface
+                    import androidx.privacysandbox.ui.core.SdkActivityLauncher
+                    @PrivacySandboxService
+                    interface MySdk
+
+                    @PrivacySandboxInterface
+                    interface MyInterface {
+                        fun useLauncher(launcher: SdkActivityLauncher)
+                    }
+                """
+        )
+
+        assertThat(compileAndParseApi(source).containsSdkActivityLauncher()).isTrue()
+    }
+
+    @Test
+    fun sandboxedUiAdapter_notDetectedWhenNotPresent() {
+        val source = Source.kotlin(
+            "com/mysdk/MySdk.kt", """
+                    import androidx.privacysandbox.tools.PrivacySandboxService
+                    // Deliberate unused import
+                    import androidx.privacysandbox.ui.core.SdkActivityLauncher
+                    @PrivacySandboxService
+                    interface MySdk {
+                        fun doStuff(input: String)
+                    }
+                """
+        )
+
+        assertThat(compileAndParseApi(source).containsSdkActivityLauncher()).isFalse()
+    }
+
+    @Test
     fun nonKotlinAnnotatedInterface_throws() {
         val source = Source.java(
             "com/mysdk/MySdk", """
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/input/com/sdk/MySdk.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/input/com/sdk/MySdk.kt
index 74b3e78..413a47a 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/input/com/sdk/MySdk.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/input/com/sdk/MySdk.kt
@@ -4,6 +4,7 @@
 import androidx.privacysandbox.tools.PrivacySandboxInterface
 import androidx.privacysandbox.tools.PrivacySandboxService
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SdkActivityLauncher
 
 @PrivacySandboxService
 interface MySdk {
@@ -21,6 +22,8 @@
     fun doSomething(firstInterface: MyInterface, secondInterface: MySecondInterface)
 
     fun doSomethingWithNullableInterface(maybeInterface: MySecondInterface?)
+
+    public fun doSomethingWithSdkActivityLauncher(launcher: SdkActivityLauncher)
 }
 
 @PrivacySandboxInterface
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterface.kt
index e04de66..cc0fb50 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterface.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterface.kt
@@ -1,9 +1,13 @@
 package com.sdk
 
+import androidx.privacysandbox.ui.core.SdkActivityLauncher
+
 public interface MyInterface {
     public suspend fun add(x: Int, y: Int): Int
 
     public fun doSomething(firstInterface: MyInterface, secondInterface: MySecondInterface): Unit
 
     public fun doSomethingWithNullableInterface(maybeInterface: MySecondInterface?): Unit
+
+    public fun doSomethingWithSdkActivityLauncher(launcher: SdkActivityLauncher): Unit
 }
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterfaceClientProxy.kt
index 05069c3..1a874d7 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterfaceClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterfaceClientProxy.kt
@@ -1,5 +1,7 @@
 package com.sdk
 
+import androidx.privacysandbox.ui.client.toLauncherInfo
+import androidx.privacysandbox.ui.core.SdkActivityLauncher
 import com.sdk.PrivacySandboxThrowableParcelConverter.fromThrowableParcel
 import kotlin.coroutines.resumeWithException
 import kotlinx.coroutines.suspendCancellableCoroutine
@@ -41,4 +43,8 @@
                 IMySecondInterfaceCoreLibInfoAndBinderWrapperConverter.toParcelable((notNullValue as
                 MySecondInterfaceClientProxy).coreLibInfo, notNullValue.remote) })
     }
+
+    public override fun doSomethingWithSdkActivityLauncher(launcher: SdkActivityLauncher): Unit {
+        remote.doSomethingWithSdkActivityLauncher(launcher.toLauncherInfo())
+    }
 }
diff --git a/privacysandbox/tools/tools-apipackager/build.gradle b/privacysandbox/tools/tools-apipackager/build.gradle
index 19089d3..006779b 100644
--- a/privacysandbox/tools/tools-apipackager/build.gradle
+++ b/privacysandbox/tools/tools-apipackager/build.gradle
@@ -15,6 +15,8 @@
  */
 
 import androidx.build.LibraryType
+import androidx.build.SdkHelperKt
+import androidx.build.SupportConfig
 
 plugins {
     id("AndroidXPlugin")
@@ -35,6 +37,13 @@
     testImplementation(project(":room:room-compiler-processing-testing"))
     testImplementation(libs.junit)
     testImplementation(libs.truth)
+
+    // TODO(b/281638337): Remove below dependency once SdkActivityLauncher stubs are removed
+    // Include android jar for compilation of generated sources.
+    testImplementation(fileTree(
+            dir: "${SdkHelperKt.getSdkPath(project)}/platforms/$SupportConfig.COMPILE_SDK_VERSION/",
+            include: "android.jar"
+    ))
 }
 
 androidx {
diff --git a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
index e537412..eb4b128 100644
--- a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
+++ b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
@@ -208,7 +208,7 @@
 
     /** Compiles the given source file and returns a classpath with the results. */
     private fun compileAndReturnUnzippedPackagedClasspath(source: Source): File {
-        val result = compileAll(listOf(source))
+        val result = compileAll(listOf(source), includeLibraryStubs = false)
         assertThat(result).succeeds()
         assertThat(result.outputClasspath).hasSize(1)
 
@@ -221,7 +221,7 @@
             generateSequence { input.nextEntry }
                 .forEach {
                     val file: File = outputDir.resolve(it.name)
-                    file.parentFile.mkdirs()
+                    file.parentFile?.mkdirs()
                     file.createNewFile()
                     input.copyTo(file.outputStream())
                 }
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt
index 2cf4f2f..abb403b 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt
@@ -118,10 +118,7 @@
 
     private fun uiAidlWrapper(annotatedInterface: AnnotatedInterface) =
         aidlParcelable(annotatedInterface.uiAdapterAidlWrapper()) {
-            addProperty(
-                "coreLibInfo",
-                AidlTypeSpec(bundleType(), kind = AidlTypeKind.PARCELABLE)
-            )
+            addProperty("coreLibInfo", bundleAidlType)
             addProperty("binder", annotatedInterface.aidlType())
         }
 
@@ -246,7 +243,12 @@
 
     private fun throwableParcelType() = Type(packageName(), throwableParcelName)
     private fun parcelableStackFrameType() = Type(packageName(), parcelableStackFrameName)
-    private fun bundleType() = Type("android.os", "Bundle")
+    private val bundleType = Type("android.os", "Bundle")
+    private val bundleAidlType =
+        AidlTypeSpec(
+            bundleType,
+            AidlTypeKind.PARCELABLE
+        )
 
     private fun transactionCallback(type: Type) =
         AidlTypeSpec(
@@ -276,6 +278,7 @@
             Short::class.qualifiedName -> primitive("int")
             Unit::class.qualifiedName -> primitive("void")
             List::class.qualifiedName -> getAidlTypeDeclaration(type.typeParameters[0]).listSpec()
+            Types.sdkActivityLauncher.qualifiedName -> bundleAidlType
             else -> throw IllegalArgumentException(
                 "Unsupported type conversion ${type.qualifiedName}"
             )
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
index 0ef84b3e..c029621 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
@@ -16,6 +16,7 @@
 
 package androidx.privacysandbox.tools.core.generator
 
+import androidx.privacysandbox.tools.core.generator.SpecNames.bundleClass
 import androidx.privacysandbox.tools.core.generator.SpecNames.contextPropertyName
 import androidx.privacysandbox.tools.core.model.AnnotatedInterface
 import androidx.privacysandbox.tools.core.model.AnnotatedValue
@@ -25,6 +26,7 @@
 import androidx.privacysandbox.tools.core.model.Types.asNonNull
 import com.squareup.kotlinpoet.ClassName
 import com.squareup.kotlinpoet.CodeBlock
+import com.squareup.kotlinpoet.MemberName
 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
 import com.squareup.kotlinpoet.TypeName
 
@@ -80,6 +82,9 @@
                     CodeBlock.of(".map { %L }", convertToModelCodeBlock)
             )
         }
+        if (type.qualifiedName == Types.sdkActivityLauncher.qualifiedName) {
+            return CodeBlock.of("SdkActivityLauncherAndBinderWrapper(%L)", expression)
+        }
         if (type == Types.short) {
             return CodeBlock.of("%L.toShort()", expression)
         }
@@ -140,6 +145,13 @@
                 toBinderList(type.typeParameters[0])
             )
         }
+        if (type.qualifiedName == Types.sdkActivityLauncher.qualifiedName) {
+            return CodeBlock.of(
+                "%L.%M()",
+                expression,
+                MemberName("androidx.privacysandbox.ui.client", "toLauncherInfo"),
+            )
+        }
         if (type == Types.short) {
             return CodeBlock.of("%L.toInt()", expression)
         }
@@ -215,6 +227,8 @@
         }
         if (type.qualifiedName == List::class.qualifiedName)
             return convertToBinderListType(type.typeParameters[0])
+        if (type.qualifiedName == Types.sdkActivityLauncher.qualifiedName)
+            return bundleClass
         return type.poetTypeName()
     }
 
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/SdkActivityLauncherWrapperGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/SdkActivityLauncherWrapperGenerator.kt
new file mode 100644
index 0000000..a90a3ef
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/SdkActivityLauncherWrapperGenerator.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.core.generator
+
+import androidx.privacysandbox.tools.core.model.Types
+import com.squareup.kotlinpoet.ClassName
+import com.squareup.kotlinpoet.CodeBlock
+import com.squareup.kotlinpoet.FileSpec
+import com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.PropertySpec
+import com.squareup.kotlinpoet.TypeSpec
+
+/**
+ * Used in server-side code generation to ensure that `SdkActivityLauncher`s are not repeatedly
+ * repackaged into binders.
+ */
+class SdkActivityLauncherWrapperGenerator(private val basePackageName: String) {
+    companion object {
+        const val className = "SdkActivityLauncherAndBinderWrapper"
+    }
+
+    fun generate(): FileSpec {
+        val classSpec = TypeSpec.classBuilder(className).build {
+            addSuperinterface(Types.sdkActivityLauncher.poetClassName(), CodeBlock.of("delegate"))
+            addModifiers(KModifier.PUBLIC)
+            primaryConstructor(
+                listOf(
+                    PropertySpec.builder(
+                        "delegate",
+                        Types.sdkActivityLauncher.poetTypeName(),
+                    ).addModifiers(KModifier.PRIVATE).build(),
+                    PropertySpec.builder(
+                        "launcherInfo",
+                        SpecNames.bundleClass,
+                    ).build(),
+                ),
+                KModifier.PRIVATE,
+            )
+            addFunction(
+                FunSpec.constructorBuilder()
+                    .addParameter("launcherInfo", SpecNames.bundleClass)
+                    .callThisConstructor(
+                        CodeBlock.of(
+                            "%T.fromLauncherInfo(launcherInfo)",
+                            ClassName(
+                                "androidx.privacysandbox.ui.provider",
+                                "SdkActivityLauncherFactory"
+                            ),
+                        ),
+                        CodeBlock.of("launcherInfo"),
+                    ).build()
+            )
+        }
+
+        return FileSpec.builder(basePackageName, className).build {
+            addCommonSettings()
+            addType(classSpec)
+        }
+    }
+}
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
index 88245e9..15d0e4b 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
@@ -30,6 +30,26 @@
         .any(Method::isSuspend)
 }
 
+fun ParsedApi.containsSdkActivityLauncher(): Boolean {
+    return values.any { it.containsSdkActivityLauncher() } ||
+        interfaces.any { it.containsSdkActivityLauncher() } ||
+        callbacks.any { it.containsSdkActivityLauncher() } ||
+        services.any { it.containsSdkActivityLauncher() }
+}
+
+private fun AnnotatedInterface.containsSdkActivityLauncher(): Boolean {
+    val isInReturns = methods
+        .any { it.returnType.qualifiedName == Types.sdkActivityLauncher.qualifiedName }
+    val isInParams = methods
+        .flatMap { it.parameters }
+        .any { it.type.qualifiedName == Types.sdkActivityLauncher.qualifiedName }
+
+    return isInReturns || isInParams
+}
+
+private fun AnnotatedValue.containsSdkActivityLauncher(): Boolean =
+    properties.any { it.type.qualifiedName == Types.sdkActivityLauncher.qualifiedName }
+
 object Types {
     val unit = Type(packageName = "kotlin", simpleName = "Unit")
     val boolean = Type(packageName = "kotlin", simpleName = "Boolean")
@@ -45,6 +65,8 @@
     val any = Type("kotlin", simpleName = "Any")
     val sandboxedUiAdapter =
         Type(packageName = "androidx.privacysandbox.ui.core", simpleName = "SandboxedUiAdapter")
+    val sdkActivityLauncher =
+        Type(packageName = "androidx.privacysandbox.ui.core", simpleName = "SdkActivityLauncher")
 
     fun list(elementType: Type) = Type(
         packageName = "kotlin.collections",
@@ -57,6 +79,7 @@
             return this
         return copy(isNullable = true)
     }
+
     fun Type.asNonNull(): Type {
         if (isNullable)
             return copy(isNullable = false)
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
index bc6aeb5..90603e5 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
@@ -147,7 +147,9 @@
     }
 
     private fun isValidInterfaceParameterType(type: Type) =
-        isValue(type) || isInterface(type) || isPrimitive(type) || isList(type) || isCallback(type)
+        isValue(type) || isInterface(type) || isPrimitive(type) || isList(type) ||
+            isCallback(type) || isBundledType(type)
+
     private fun isValidInterfaceReturnType(type: Type) =
         isValue(type) || isInterface(type) || isPrimitive(type) || isList(type)
     private fun isValidValuePropertyType(type: Type) =
@@ -171,6 +173,8 @@
         }
         return false
     }
+
+    private fun isBundledType(type: Type) = type == Types.sdkActivityLauncher
 }
 
 data class ValidationResult(val errors: List<String>) {
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlInterfaceGeneratorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlInterfaceGeneratorTest.kt
index 780e27c..a536da8 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlInterfaceGeneratorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlInterfaceGeneratorTest.kt
@@ -72,7 +72,15 @@
                             parameters = listOf(),
                             returnType = Type("com.mysdk", "MyInterface"),
                             isSuspend = true,
-                        )
+                        ),
+                        Method(
+                            name = "methodWithActivityLauncherParam",
+                            parameters = listOf(
+                                Parameter("activityLauncher", Types.sdkActivityLauncher)
+                            ),
+                            returnType = Types.unit,
+                            isSuspend = false,
+                        ),
                     )
                 )
             )
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterface.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterface.aidl
index 2a532b7..4815029 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterface.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterface.aidl
@@ -1,8 +1,10 @@
 package com.mysdk;
 
+import android.os.Bundle;
 import com.mysdk.IMyInterfaceTransactionCallback;
 
 oneway interface IMyInterface {
+    void methodWithActivityLauncherParam(in Bundle activityLauncher) = 12556385;
     void methodWithInterfaceParam(IMyInterface myInterface) = 5537946;
     void suspendMethodWithInterfaceReturn(IMyInterfaceTransactionCallback transactionCallback) = 13841773;
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
index 0a9c958..175800e 100644
--- a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
+++ b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
@@ -40,13 +40,17 @@
         sources: List<Source>,
         extraClasspath: List<File> = emptyList(),
         symbolProcessorProviders: List<SymbolProcessorProvider> = emptyList(),
-        processorOptions: Map<String, String> = emptyMap()
+        processorOptions: Map<String, String> = emptyMap(),
+        includeLibraryStubs: Boolean = true,
     ): TestCompilationResult {
         val tempDir = Files.createTempDirectory("compile").toFile().also { it.deleteOnExit() }
+        // TODO(b/281638337): Remove library stubs once SdkActivityLauncher is upstreamed
+        val fullSources = sources +
+            if (includeLibraryStubs) syntheticUiLibraryStubs else emptyList()
         return compile(
             tempDir,
             TestCompilationArguments(
-                sources = sources,
+                sources = fullSources,
                 classpath = extraClasspath,
                 symbolProcessorProviders = symbolProcessorProviders,
                 processorOptions = processorOptions,
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/LibraryStubs.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/LibraryStubs.kt
new file mode 100644
index 0000000..95116e5
--- /dev/null
+++ b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/LibraryStubs.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.testing
+
+import androidx.room.compiler.processing.util.Source
+
+val syntheticUiLibraryStubs = listOf(
+    Source.kotlin(
+        "androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt", """
+        |package androidx.privacysandbox.ui.core
+        |
+        |import android.os.IBinder
+        |
+        |interface SdkActivityLauncher {
+        |    suspend fun launchSdkActivity(sdkActivityHandlerToken: IBinder): Boolean
+        |}
+        |""".trimMargin()
+    ),
+    Source.kotlin(
+        "androidx/privacysandbox/ui/client/SdkActivityLaunchers.kt", """
+        |@file:JvmName("SdkActivityLaunchers")
+        |
+        |package androidx.privacysandbox.ui.client
+        |
+        |import android.os.Bundle
+        |import androidx.privacysandbox.ui.core.SdkActivityLauncher
+        |
+        |fun SdkActivityLauncher.toLauncherInfo(): Bundle {
+        |    TODO("Stub!")
+        |}
+        |""".trimMargin()
+    ),
+    Source.kotlin(
+        "androidx/privacysandbox/ui/provider/SdkActivityLauncherFactory.kt", """
+        |package androidx.privacysandbox.ui.provider
+        |
+        |import android.os.Bundle
+        |import androidx.privacysandbox.ui.core.SdkActivityLauncher
+        |
+        |object SdkActivityLauncherFactory {
+        |
+        |    @JvmStatic
+        |    @Suppress("UNUSED_PARAMETER")
+        |    fun fromLauncherInfo(launcherInfo: Bundle): SdkActivityLauncher {
+        |        TODO("Stub!")
+        |    }
+        |}""".trimMargin()
+    ),
+    Source.kotlin(
+        "androidx/core/os/BundleCompat.kt", """
+        |package androidx.core.os
+        |
+        |import android.os.IBinder
+        |import android.os.Bundle
+        |
+        |object BundleCompat {
+        |    @Suppress("UNUSED_PARAMETER")
+        |    fun getBinder(bundle: Bundle, key: String?): IBinder? {
+        |        TODO("Stub!")
+        |    }
+        |}
+        |""".trimMargin()
+    ),
+)
diff --git a/settings.gradle b/settings.gradle
index 0f11a3a..063496e 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1012,11 +1012,13 @@
 includeProject(":wear:compose:compose-material3-benchmark", "wear/compose/compose-material3/benchmark", [BuildType.COMPOSE])
 includeProject(":wear:compose:compose-material-core", [BuildType.COMPOSE])
 includeProject(":wear:compose:compose-material-samples", "wear/compose/compose-material/samples", [BuildType.COMPOSE])
+includeProject(":wear:compose:compose-material3-integration-tests", "wear/compose/compose-material3/integration-tests", [BuildType.COMPOSE])
 includeProject(":wear:compose:compose-material3-samples", "wear/compose/compose-material3/samples", [BuildType.COMPOSE])
 includeProject(":wear:compose:compose-navigation", [BuildType.COMPOSE])
 includeProject(":wear:compose:compose-navigation-samples", "wear/compose/compose-navigation/samples", [BuildType.COMPOSE])
 includeProject(":wear:compose:compose-ui-tooling", [BuildType.COMPOSE])
 includeProject(":wear:compose:integration-tests:demos", [BuildType.COMPOSE])
+includeProject(":wear:compose:integration-tests:demos:common", [BuildType.COMPOSE])
 includeProject(":wear:compose:integration-tests:macrobenchmark", [BuildType.COMPOSE])
 includeProject(":wear:compose:integration-tests:macrobenchmark-target", [BuildType.COMPOSE])
 includeProject(":wear:compose:integration-tests:navigation", [BuildType.COMPOSE])
diff --git a/wear/compose/compose-material3/integration-tests/build.gradle b/wear/compose/compose-material3/integration-tests/build.gradle
new file mode 100644
index 0000000..b139b83
--- /dev/null
+++ b/wear/compose/compose-material3/integration-tests/build.gradle
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.Publish
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("AndroidXComposePlugin")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    implementation(libs.kotlinStdlib)
+
+    implementation(project(":compose:animation:animation"))
+    implementation(project(":compose:foundation:foundation"))
+    implementation(project(":compose:foundation:foundation-layout"))
+    implementation(project(":compose:runtime:runtime"))
+    implementation(project(":compose:ui:ui"))
+    implementation(project(":compose:ui:ui-text"))
+
+    implementation(project(':wear:compose:compose-foundation'))
+    implementation(project(':wear:compose:compose-material3'))
+    implementation(project(':wear:compose:integration-tests:demos:common'))
+}
+
+androidx {
+    name = "AndroidX Wear Compose Material3 Components Demos"
+    publish = Publish.NONE
+    inceptionYear = "2023"
+    description = "Contains the demo code for the AndroidX Wear Compose Material 3 components."
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 25
+    }
+    namespace "androidx.wear.compose.material3.demos"
+}
\ No newline at end of file
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
new file mode 100644
index 0000000..6492902
--- /dev/null
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.demos
+
+import androidx.wear.compose.integration.demos.common.DemoCategory
+
+val WearMaterial3Demos = DemoCategory(
+    "Material3",
+    listOf(
+        DemoCategory(
+            "ScrollAway",
+            listOf(
+                // Add Material 3 demos here
+            )
+        ),
+    )
+)
\ No newline at end of file
diff --git a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/Button.kt b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/Button.kt
index 7503da4..4523dc5 100644
--- a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/Button.kt
+++ b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/Button.kt
@@ -803,8 +803,8 @@
     fun outlinedButtonBorder(
         enabled: Boolean,
         borderColor: Color = MaterialTheme.colorScheme.outline,
-        disabledBorderColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = DisabledBorderAndContainerAlpha
+        disabledBorderColor: Color = MaterialTheme.colorScheme.onSurface.toDisabledColor(
+            disabledAlpha = DisabledBorderAndContainerAlpha
         ),
         borderWidth: Dp = 1.dp
     ): BorderStroke {
@@ -853,18 +853,13 @@
         contentColor: Color = contentColorFor(containerColor),
         secondaryContentColor: Color = contentColor,
         iconColor: Color = contentColor,
-        disabledContainerColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = DisabledBorderAndContainerAlpha
+        disabledContainerColor: Color = MaterialTheme.colorScheme.onSurface.toDisabledColor(
+            disabledAlpha = DisabledBorderAndContainerAlpha
         ),
-        disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = ContentAlpha.disabled
-        ),
-        disabledSecondaryContentColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = ContentAlpha.disabled
-        ),
-        disabledIconColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = ContentAlpha.disabled
-        ),
+        disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.toDisabledColor(),
+        disabledSecondaryContentColor: Color =
+            MaterialTheme.colorScheme.onSurface.toDisabledColor(),
+        disabledIconColor: Color = MaterialTheme.colorScheme.onSurface.toDisabledColor()
     ): ButtonColors = ButtonColors(
         containerColor = containerColor,
         contentColor = contentColor,
diff --git a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/ColorScheme.kt b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/ColorScheme.kt
index 1216db9..1ee0789 100644
--- a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/ColorScheme.kt
+++ b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/ColorScheme.kt
@@ -454,3 +454,11 @@
 }
 
 internal val LocalColors = staticCompositionLocalOf<ColorScheme> { ColorScheme() }
+
+/**
+ * Convert given color to disabled color.
+ * @param disabledAlpha Alpha used to represent disabled colors.
+ */
+@Composable
+internal fun Color.toDisabledColor(disabledAlpha: Float = ContentAlpha.disabled) =
+    this.copy(alpha = this.alpha * disabledAlpha)
diff --git a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/IconButton.kt b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/IconButton.kt
index d4b2a43..75dd372 100644
--- a/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/IconButton.kt
+++ b/wear/compose/compose-material3/src/commonMain/kotlin/androidx/wear/compose/material3/IconButton.kt
@@ -332,12 +332,10 @@
     fun iconButtonColors(
         containerColor: Color = Color.Transparent,
         contentColor: Color = MaterialTheme.colorScheme.onBackground,
-        disabledContainerColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = DisabledBorderAndContainerAlpha
+        disabledContainerColor: Color = MaterialTheme.colorScheme.onSurface.toDisabledColor(
+            disabledAlpha = DisabledBorderAndContainerAlpha
         ),
-        disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = ContentAlpha.disabled
-        )
+        disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.toDisabledColor()
     ): IconButtonColors = IconButtonColors(
         containerColor = containerColor,
         contentColor = contentColor,
@@ -359,8 +357,8 @@
     fun outlinedIconButtonBorder(
         enabled: Boolean,
         borderColor: Color = MaterialTheme.colorScheme.outline,
-        disabledBorderColor: Color = MaterialTheme.colorScheme.onSurface.copy(
-            alpha = DisabledBorderAndContainerAlpha
+        disabledBorderColor: Color = MaterialTheme.colorScheme.onSurface.toDisabledColor(
+            disabledAlpha = DisabledBorderAndContainerAlpha
         ),
         borderWidth: Dp = 1.dp
     ): BorderStroke {
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 478b99e..0d46a1e 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -63,6 +63,8 @@
     implementation(project(":wear:compose:compose-foundation-samples"))
     implementation(project(':wear:compose:compose-material'))
     implementation(project(":wear:compose:compose-material-samples"))
+    implementation(project(':wear:compose:integration-tests:demos:common'))
+    implementation(project(":wear:compose:compose-material3-integration-tests"))
 
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
     androidTestImplementation(project(":emoji2:emoji2"))
diff --git a/wear/compose/integration-tests/demos/common/build.gradle b/wear/compose/integration-tests/demos/common/build.gradle
new file mode 100644
index 0000000..9e6cd01e
--- /dev/null
+++ b/wear/compose/integration-tests/demos/common/build.gradle
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+    id("AndroidXPlugin")
+    id("AndroidXComposePlugin")
+    id("com.android.library")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    implementation(libs.kotlinStdlib)
+
+    api("androidx.activity:activity:1.2.0")
+    implementation(project(':wear:compose:compose-material'))
+
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 25
+    }
+    namespace "androidx.wear.compose.integration.demos.common"
+}
diff --git a/wear/compose/integration-tests/demos/common/src/main/java/androidx/wear/compose/integration/demos/common/Demo.kt b/wear/compose/integration-tests/demos/common/src/main/java/androidx/wear/compose/integration/demos/common/Demo.kt
new file mode 100644
index 0000000..52a987a
--- /dev/null
+++ b/wear/compose/integration-tests/demos/common/src/main/java/androidx/wear/compose/integration/demos/common/Demo.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.integration.demos.common
+
+import android.app.Activity
+import androidx.activity.ComponentActivity
+import androidx.compose.runtime.Composable
+import androidx.wear.compose.material.SwipeToDismissBoxState
+import kotlin.reflect.KClass
+
+/**
+ * Generic demo with a [title] that will be displayed in the list of demos.
+ */
+sealed class Demo(val title: String, val description: String? = null) {
+    override fun toString() = title
+}
+
+/**
+ * Demo that launches an [Activity] when selected.
+ *
+ * This should only be used for demos that need to customize the activity, the large majority of
+ * demos should just use [ComposableDemo] instead.
+ *
+ * @property activityClass the KClass (Foo::class) of the activity that will be launched when
+ * this demo is selected.
+ */
+class ActivityDemo<T : ComponentActivity>(title: String, val activityClass: KClass<T>) : Demo(title)
+
+/**
+ * A category of [Demo]s, that will display a list of [demos] when selected.
+ */
+class DemoCategory(
+    title: String,
+    val demos: List<Demo>
+) : Demo(title)
+
+/**
+ * Parameters which are used by [Demo] screens.
+ */
+class DemoParameters(
+    val navigateBack: () -> Unit,
+    val swipeToDismissBoxState: SwipeToDismissBoxState
+)
+
+/**
+ * Demo that displays [Composable] [content] when selected,
+ * with a method to navigate back to the parent.
+ */
+class ComposableDemo(
+    title: String,
+    description: String? = null,
+    val content: @Composable (params: DemoParameters) -> Unit,
+) : Demo(title, description)
diff --git a/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt b/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt
index 9fe82bca..646addf 100644
--- a/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt
+++ b/wear/compose/integration-tests/demos/src/androidTest/java/androidx/wear/compose/integration/demos/test/DemoTest.kt
@@ -28,10 +28,10 @@
 import androidx.test.espresso.Espresso
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
-import androidx.wear.compose.integration.demos.Demo
 import androidx.wear.compose.integration.demos.DemoActivity
-import androidx.wear.compose.integration.demos.DemoCategory
 import androidx.wear.compose.integration.demos.WearComposeDemos
+import androidx.wear.compose.integration.demos.common.Demo
+import androidx.wear.compose.integration.demos.common.DemoCategory
 import com.google.common.truth.Truth.assertThat
 import org.junit.Ignore
 import org.junit.Rule
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoActivity.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoActivity.kt
index 361fb0e..7de734f 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoActivity.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoActivity.kt
@@ -36,6 +36,9 @@
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.LocalView
 import androidx.core.app.ActivityCompat
+import androidx.wear.compose.integration.demos.common.ActivityDemo
+import androidx.wear.compose.integration.demos.common.Demo
+import androidx.wear.compose.integration.demos.common.DemoCategory
 import androidx.wear.compose.material.MaterialTheme
 
 /**
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
index 108b9ad..7e6aa69 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoApp.kt
@@ -32,7 +32,6 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.focus.FocusRequester
@@ -49,6 +48,11 @@
 import androidx.wear.compose.foundation.lazy.ScalingLazyListState
 import androidx.wear.compose.foundation.lazy.ScalingParams
 import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
+import androidx.wear.compose.integration.demos.common.ActivityDemo
+import androidx.wear.compose.integration.demos.common.ComposableDemo
+import androidx.wear.compose.integration.demos.common.Demo
+import androidx.wear.compose.integration.demos.common.DemoCategory
+import androidx.wear.compose.integration.demos.common.DemoParameters
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipDefaults
 import androidx.wear.compose.material.ListHeader
@@ -154,13 +158,13 @@
                     modifier = Modifier.fillMaxWidth()
                 )
             }
-            if (demo.description != null) {
+            demo.description?.let { description ->
                 item {
                     CompositionLocalProvider(
                         LocalTextStyle provides MaterialTheme.typography.caption3
                     ) {
                         Text(
-                            text = demo.description,
+                            text = description,
                             modifier = Modifier.fillMaxWidth().align(Alignment.Center),
                             textAlign = TextAlign.Center
                         )
@@ -187,7 +191,6 @@
 
 internal data class TimestampedDelta(val time: Long, val delta: Float)
 
-@OptIn(ExperimentalComposeUiApi::class)
 @Suppress("ComposableModifierFactory")
 @Composable
 fun Modifier.rsbScroll(
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoComponents.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoComponents.kt
index ea5174c..4876340 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoComponents.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/DemoComponents.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,8 +16,6 @@
 
 package androidx.wear.compose.integration.demos
 
-import android.app.Activity
-import androidx.activity.ComponentActivity
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -43,53 +41,7 @@
 import androidx.wear.compose.material.Icon
 import androidx.wear.compose.material.LocalContentAlpha
 import androidx.wear.compose.material.MaterialTheme
-import androidx.wear.compose.material.SwipeToDismissBoxState
 import androidx.wear.compose.material.Text
-import kotlin.reflect.KClass
-
-/**
- * Generic demo with a [title] that will be displayed in the list of demos.
- */
-sealed class Demo(val title: String, val description: String? = null) {
-    override fun toString() = title
-}
-
-/**
- * Demo that launches an [Activity] when selected.
- *
- * This should only be used for demos that need to customize the activity, the large majority of
- * demos should just use [ComposableDemo] instead.
- *
- * @property activityClass the KClass (Foo::class) of the activity that will be launched when
- * this demo is selected.
- */
-class ActivityDemo<T : ComponentActivity>(title: String, val activityClass: KClass<T>) : Demo(title)
-
-/**
- * A category of [Demo]s, that will display a list of [demos] when selected.
- */
-class DemoCategory(
-    title: String,
-    val demos: List<Demo>
-) : Demo(title)
-
-/**
- * Parameters which are used by [Demo] screens.
- */
-class DemoParameters(
-    val navigateBack: () -> Unit,
-    val swipeToDismissBoxState: SwipeToDismissBoxState
-)
-
-/**
- * Demo that displays [Composable] [content] when selected,
- * with a method to navigate back to the parent.
- */
-class ComposableDemo(
-    title: String,
-    description: String? = null,
-    val content: @Composable (params: DemoParameters) -> Unit,
-) : Demo(title, description)
 
 /**
  * A simple [Icon] with default size
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/Demos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/Demos.kt
index 6fa889e..8efc582 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/Demos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/Demos.kt
@@ -23,7 +23,10 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.text.style.TextAlign
+import androidx.wear.compose.integration.demos.common.ComposableDemo
+import androidx.wear.compose.integration.demos.common.DemoCategory
 import androidx.wear.compose.material.Text
+import androidx.wear.compose.material3.demos.WearMaterial3Demos
 
 val Info = DemoCategory(
     "App Info",
@@ -56,6 +59,7 @@
     listOf(
         WearFoundationDemos,
         WearMaterialDemos,
+        WearMaterial3Demos,
         Info
     )
 )
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
index c54950a..730a272 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/FoundationDemos.kt
@@ -34,6 +34,8 @@
 import androidx.wear.compose.foundation.samples.SimpleScalingLazyColumnWithContentPadding
 import androidx.wear.compose.foundation.samples.SimpleScalingLazyColumnWithSnap
 import androidx.wear.compose.foundation.samples.SwipeToRevealWithExpandables
+import androidx.wear.compose.integration.demos.common.ComposableDemo
+import androidx.wear.compose.integration.demos.common.DemoCategory
 
 val WearFoundationDemos = DemoCategory(
     "Foundation",
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
index 205da0d..82d84b9 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
@@ -27,6 +27,8 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.integration.demos.common.ComposableDemo
+import androidx.wear.compose.integration.demos.common.DemoCategory
 import androidx.wear.compose.material.samples.AlertDialogSample
 import androidx.wear.compose.material.samples.AlertWithButtons
 import androidx.wear.compose.material.samples.AlertWithChips
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollingWithRotaryInputDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollingWithRotaryInputDemo.kt
index 7153372..33c1b57 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollingWithRotaryInputDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollingWithRotaryInputDemo.kt
@@ -26,6 +26,7 @@
 import androidx.compose.ui.samples.PreRotaryEventSample
 import androidx.compose.ui.samples.RotaryEventSample
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.integration.demos.common.ComposableDemo
 import androidx.wear.compose.material.Text
 
 internal val RotaryInputDemos = listOf(
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
index 5893701..1d69a87 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
@@ -26,8 +26,8 @@
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32;
 import androidx.wear.protolayout.expression.DynamicDataKey;
 import androidx.wear.protolayout.expression.PlatformHealthSources;
-import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway;
 import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
+import androidx.wear.protolayout.expression.proto.DynamicDataProto.DynamicDataValue;
 import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableFixedInt32;
 import androidx.wear.protolayout.expression.proto.DynamicProto.ArithmeticInt32Op;
 import androidx.wear.protolayout.expression.proto.DynamicProto.DurationPartType;
@@ -39,6 +39,7 @@
 import androidx.wear.protolayout.expression.proto.FixedProto.FixedInt32;
 
 import java.time.Duration;
+import java.util.function.Function;
 
 /** Dynamic data nodes which yield integers. */
 class Int32Nodes {
@@ -83,7 +84,7 @@
             super(
                     stateStore,
                     getDataKey(protoNode.getSourceType()),
-                    se -> se.getInt32Val().getValue(),
+                    getStateExtractor(protoNode.getSourceType()),
                     downstream);
         }
 
@@ -100,6 +101,21 @@
             throw new IllegalArgumentException(
                     "Unknown DynamicInt32 platform source type: " + type);
         }
+
+        @NonNull
+        private static Function<DynamicDataValue, Integer> getStateExtractor(
+                PlatformInt32SourceType type) {
+            if (type == PlatformInt32SourceType.PLATFORM_INT32_SOURCE_TYPE_CURRENT_HEART_RATE) {
+                return se -> (int) se.getFloatVal().getValue();
+            }
+
+            if (type == PlatformInt32SourceType.PLATFORM_INT32_SOURCE_TYPE_DAILY_STEP_COUNT) {
+                return se -> se.getInt32Val().getValue();
+            }
+
+            throw new IllegalArgumentException(
+                    "Unknown DynamicInt32 platform source type: " + type);
+        }
     }
 
     /** Dynamic integer node that supports arithmetic operations. */
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/SensorGatewaySingleDataProvider.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/SensorGatewaySingleDataProvider.java
index ee1671e..564bab1 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/SensorGatewaySingleDataProvider.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/SensorGatewaySingleDataProvider.java
@@ -22,11 +22,13 @@
 import androidx.annotation.RestrictTo.Scope;
 import androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue;
 import androidx.wear.protolayout.expression.PlatformDataKey;
+import androidx.wear.protolayout.expression.PlatformHealthSources;
 import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway;
 
 import java.util.Collections;
 import java.util.Map;
 import java.util.concurrent.Executor;
+import java.util.function.Function;
 
 /** This provider provides sensor data as state value. */
 @RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
@@ -35,12 +37,20 @@
     @NonNull final PlatformDataKey<?> mSupportedKey;
     @Nullable private SensorGateway.Consumer mSensorGatewayConsumer = null;
 
+    @NonNull Function<Double, DynamicDataValue> mConvertFunc;
+
     public SensorGatewaySingleDataProvider(
             @NonNull SensorGateway sensorGateway,
             @NonNull PlatformDataKey<?> supportedKey
     ) {
         this.mSensorGateway = sensorGateway;
         this.mSupportedKey = supportedKey;
+
+        if (mSupportedKey.equals(PlatformHealthSources.HEART_RATE_BPM)) {
+            mConvertFunc = value -> DynamicDataValue.fromFloat(value.floatValue());
+        } else { // mSupportedKey.equals(PlatformHealthSources.DAILY_STEPS)
+            mConvertFunc = value -> DynamicDataValue.fromInt(value.intValue());
+        }
     }
 
     @Override
@@ -52,7 +62,7 @@
                     @Override
                     public void onData(double value) {
                         executor.execute(() -> callback.onData(
-                                Map.of(mSupportedKey, DynamicDataValue.fromFloat((float) value)))
+                                Map.of(mSupportedKey, mConvertFunc.apply(value)))
                         );
                     }
 
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java
index 1cd0634..ef45da0 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java
@@ -29,6 +29,7 @@
 class StateSourceNode<T>
         implements DynamicDataSourceNode<T>,
         DynamicTypeValueReceiverWithPreUpdate<DynamicDataValue> {
+    @NonNull private static final String RESERVED_NAMESPACE = "protolayout";
     private final StateStore mStateStore;
     private final DynamicDataKey<?> mKey;
     private final Function<DynamicDataValue, T> mStateExtractor;
@@ -92,6 +93,11 @@
         if (namespace.isEmpty()) {
             return new AppDataKey<T>(key);
         }
+
+        if (RESERVED_NAMESPACE.equalsIgnoreCase(namespace)) {
+            return new PlatformDataKey<T>(key);
+        }
+
         return new PlatformDataKey<T>(namespace, key);
     }
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/FakeSensorGateway.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/FakeSensorGateway.java
new file mode 100644
index 0000000..c661e3f
--- /dev/null
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/FakeSensorGateway.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.expression.pipeline;
+
+import androidx.annotation.NonNull;
+import androidx.wear.protolayout.expression.PlatformDataKey;
+import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+class FakeSensorGateway implements SensorGateway {
+    final List<Consumer> registeredConsumers = new ArrayList<>();
+
+    @Override
+    public void enableUpdates() {
+    }
+
+    @Override
+    public void disableUpdates() {
+    }
+
+    @Override
+    public void registerSensorGatewayConsumer(
+            @NonNull PlatformDataKey<?> key, @NonNull Consumer consumer) {
+        registeredConsumers.add(consumer);
+    }
+
+    @Override
+    public void registerSensorGatewayConsumer(
+            @NonNull PlatformDataKey<?> key,
+            @NonNull Executor executor,
+            @NonNull Consumer consumer) {
+        registerSensorGatewayConsumer(key, consumer);
+    }
+
+    @Override
+    public void unregisterSensorGatewayConsumer(
+            @NonNull PlatformDataKey<?> key, @NonNull Consumer consumer) {
+        registeredConsumers.remove(consumer);
+    }
+}
\ No newline at end of file
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/FloatNodeTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/FloatNodeTest.java
index a1acb98..4f2b87a 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/FloatNodeTest.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/FloatNodeTest.java
@@ -24,9 +24,11 @@
 
 import android.os.Looper;
 
+import androidx.collection.ArrayMap;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat;
 import androidx.wear.protolayout.expression.AppDataKey;
+import androidx.wear.protolayout.expression.PlatformHealthSources;
 import androidx.wear.protolayout.expression.pipeline.FloatNodes.AnimatableFixedFloatNode;
 import androidx.wear.protolayout.expression.pipeline.FloatNodes.ArithmeticFloatNode;
 import androidx.wear.protolayout.expression.pipeline.FloatNodes.DynamicAnimatedFloatNode;
@@ -51,6 +53,7 @@
 import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 @RunWith(AndroidJUnit4.class)
@@ -127,6 +130,40 @@
     }
 
     @Test
+    public void stateFloatSource_canSubscribeToHeartRateUpdates() {
+        FakeSensorGateway fakeSensorGateway = new FakeSensorGateway();
+        StateStore stateStore = new StateStore(new ArrayMap<>());
+        stateStore.putAllPlatformProviders(
+                Collections.singletonMap(
+                        PlatformHealthSources.HEART_RATE_BPM,
+                        new SensorGatewaySingleDataProvider(
+                                fakeSensorGateway, PlatformHealthSources.HEART_RATE_BPM)));
+        StateFloatSource dailyStepsSource =
+                StateFloatSource.newBuilder()
+                        .setSourceKey(PlatformHealthSources.HEART_RATE_BPM.getKey())
+                        .setSourceNamespace(PlatformHealthSources.HEART_RATE_BPM.getNamespace())
+                        .build();
+        List<Float> results = new ArrayList<>();
+        StateFloatSourceNode dailyStepsSourceNode =
+                new StateFloatSourceNode(
+                        stateStore,
+                        dailyStepsSource,
+                        new AddToListCallback<>(results));
+
+        dailyStepsSourceNode.preInit();
+        dailyStepsSourceNode.init();
+        assertThat(fakeSensorGateway.registeredConsumers).hasSize(1);
+
+        fakeSensorGateway.registeredConsumers.get(0).onData(70.0);
+        assertThat(results).hasSize(1);
+        assertThat(results).containsExactly(70.0f);
+
+        fakeSensorGateway.registeredConsumers.get(0).onData(80.0);
+        assertThat(results).hasSize(2);
+        assertThat(results).containsExactly(70.0f, 80.0f);
+    }
+
+    @Test
     public void stateFloatSourceNode_noUpdatesAfterDestroy() {
         List<Float> results = new ArrayList<>();
         float oldValue = 6.5f;
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/Int32NodesTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/Int32NodesTest.java
index 2738f50..043eab7 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/Int32NodesTest.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/Int32NodesTest.java
@@ -25,12 +25,10 @@
 
 import android.os.Looper;
 
-import androidx.annotation.NonNull;
 import androidx.collection.ArrayMap;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.wear.protolayout.expression.AppDataKey;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32;
-import androidx.wear.protolayout.expression.PlatformDataKey;
 import androidx.wear.protolayout.expression.PlatformHealthSources;
 import androidx.wear.protolayout.expression.pipeline.Int32Nodes.AnimatableFixedInt32Node;
 import androidx.wear.protolayout.expression.pipeline.Int32Nodes.DynamicAnimatedInt32Node;
@@ -38,7 +36,6 @@
 import androidx.wear.protolayout.expression.pipeline.Int32Nodes.GetDurationPartOpNode;
 import androidx.wear.protolayout.expression.pipeline.Int32Nodes.LegacyPlatformInt32SourceNode;
 import androidx.wear.protolayout.expression.pipeline.Int32Nodes.StateInt32SourceNode;
-import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway;
 import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
 import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableFixedInt32;
 import androidx.wear.protolayout.expression.proto.DynamicProto.DurationPartType;
@@ -230,6 +227,40 @@
     }
 
     @Test
+    public void stateInt32Source_canSubscribeToDailyStepsUpdates() {
+        FakeSensorGateway fakeSensorGateway = new FakeSensorGateway();
+        StateStore stateStore = new StateStore(new ArrayMap<>());
+        stateStore.putAllPlatformProviders(
+                Collections.singletonMap(
+                        PlatformHealthSources.DAILY_STEPS,
+                        new SensorGatewaySingleDataProvider(
+                                fakeSensorGateway, PlatformHealthSources.DAILY_STEPS)));
+        StateInt32Source dailyStepsSource =
+                StateInt32Source.newBuilder()
+                        .setSourceKey(PlatformHealthSources.DAILY_STEPS.getKey())
+                        .setSourceNamespace(PlatformHealthSources.DAILY_STEPS.getNamespace())
+                        .build();
+        List<Integer> results = new ArrayList<>();
+        StateInt32SourceNode dailyStepsSourceNode =
+                new StateInt32SourceNode(
+                        stateStore,
+                        dailyStepsSource,
+                        new AddToListCallback<>(results));
+
+        dailyStepsSourceNode.preInit();
+        dailyStepsSourceNode.init();
+        assertThat(fakeSensorGateway.registeredConsumers).hasSize(1);
+
+        fakeSensorGateway.registeredConsumers.get(0).onData(70);
+        assertThat(results).hasSize(1);
+        assertThat(results).containsExactly(70);
+
+        fakeSensorGateway.registeredConsumers.get(0).onData(80);
+        assertThat(results).hasSize(2);
+        assertThat(results).containsExactly(70, 80);
+    }
+
+    @Test
     public void animatableFixedInt32_animates() {
         int startValue = 3;
         int endValue = 33;
@@ -363,6 +394,76 @@
     }
 
     @Test
+    public void platformInt32Source_canSubscribeToHeartRateUpdates() {
+        FakeSensorGateway fakeSensorGateway = new FakeSensorGateway();
+        StateStore stateStore = new StateStore(new ArrayMap<>());
+        stateStore.putAllPlatformProviders(
+                Collections.singletonMap(
+                        PlatformHealthSources.HEART_RATE_BPM,
+                        new SensorGatewaySingleDataProvider(
+                                fakeSensorGateway, PlatformHealthSources.HEART_RATE_BPM)));
+        PlatformInt32Source platformSource =
+                PlatformInt32Source.newBuilder()
+                        .setSourceType(
+                                PlatformInt32SourceType
+                                        .PLATFORM_INT32_SOURCE_TYPE_CURRENT_HEART_RATE)
+                        .build();
+        List<Integer> results = new ArrayList<>();
+        LegacyPlatformInt32SourceNode platformSourceNode =
+                new LegacyPlatformInt32SourceNode(
+                        stateStore,
+                        platformSource,
+                        new AddToListCallback<>(results));
+
+        platformSourceNode.preInit();
+        platformSourceNode.init();
+        assertThat(fakeSensorGateway.registeredConsumers).hasSize(1);
+
+        fakeSensorGateway.registeredConsumers.get(0).onData(70);
+        assertThat(results).hasSize(1);
+        assertThat(results).containsExactly(70);
+
+        fakeSensorGateway.registeredConsumers.get(0).onData(80);
+        assertThat(results).hasSize(2);
+        assertThat(results).containsExactly(70, 80);
+    }
+
+    @Test
+    public void platformInt32Source_canSubscribeToDailyStepsUpdates() {
+        FakeSensorGateway fakeSensorGateway = new FakeSensorGateway();
+        StateStore stateStore = new StateStore(new ArrayMap<>());
+        stateStore.putAllPlatformProviders(
+                Collections.singletonMap(
+                        PlatformHealthSources.DAILY_STEPS,
+                        new SensorGatewaySingleDataProvider(
+                                fakeSensorGateway, PlatformHealthSources.DAILY_STEPS)));
+        PlatformInt32Source platformSource =
+                PlatformInt32Source.newBuilder()
+                        .setSourceType(
+                                PlatformInt32SourceType
+                                        .PLATFORM_INT32_SOURCE_TYPE_DAILY_STEP_COUNT)
+                        .build();
+        List<Integer> results = new ArrayList<>();
+        LegacyPlatformInt32SourceNode platformSourceNode =
+                new LegacyPlatformInt32SourceNode(
+                        stateStore,
+                        platformSource,
+                        new AddToListCallback<>(results));
+
+        platformSourceNode.preInit();
+        platformSourceNode.init();
+        assertThat(fakeSensorGateway.registeredConsumers).hasSize(1);
+
+        fakeSensorGateway.registeredConsumers.get(0).onData(70.0);
+        assertThat(results).hasSize(1);
+        assertThat(results).containsExactly(70);
+
+        fakeSensorGateway.registeredConsumers.get(0).onData(80.0);
+        assertThat(results).hasSize(2);
+        assertThat(results).containsExactly(70, 80);
+    }
+
+    @Test
     public void platformInt32Source_propagatesInvalidatedSignal() {
         FakeSensorGateway fakeSensorGateway = new FakeSensorGateway();
         StateStore stateStore = new StateStore(new ArrayMap<>());
@@ -392,36 +493,4 @@
         fakeSensorGateway.registeredConsumers.get(0).onInvalidated();
         verify(mMockValueReceiver).onInvalidated();
     }
-
-    private static class FakeSensorGateway implements SensorGateway {
-        final List<Consumer> registeredConsumers = new ArrayList<>();
-
-        @Override
-        public void enableUpdates() {
-        }
-
-        @Override
-        public void disableUpdates() {
-        }
-
-        @Override
-        public void registerSensorGatewayConsumer(
-                @NonNull PlatformDataKey<?> key, @NonNull Consumer consumer) {
-            registeredConsumers.add(consumer);
-        }
-
-        @Override
-        public void registerSensorGatewayConsumer(
-                @NonNull PlatformDataKey<?> key,
-                @NonNull Executor executor,
-                @NonNull Consumer consumer) {
-            registerSensorGatewayConsumer(key, consumer);
-        }
-
-        @Override
-        public void unregisterSensorGatewayConsumer(
-                @NonNull PlatformDataKey<?> key, @NonNull Consumer consumer) {
-            registeredConsumers.remove(consumer);
-        }
-    }
 }
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index 7f2e9cb..1ca79e0 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -104,10 +104,13 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool from(androidx.wear.protolayout.expression.DynamicDataKey<androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool!>);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromByteArray(byte[], int, int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool negate();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool or(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicBoolByteArray();
+    method public default int toDynamicBoolByteArray(byte[]);
+    method public default int toDynamicBoolByteArray(byte[], int, int);
   }
 
   public static interface DynamicBuilders.DynamicColor extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
@@ -120,18 +123,24 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor constant(@ColorInt int);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor from(androidx.wear.protolayout.expression.DynamicDataKey<androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor!>);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromByteArray(byte[], int, int);
     method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor!,java.lang.Integer!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicColorByteArray();
+    method public default int toDynamicColorByteArray(byte[]);
+    method public default int toDynamicColorByteArray(byte[], int, int);
   }
 
   public static interface DynamicBuilders.DynamicDuration extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicDuration fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicDuration fromByteArray(byte[], int, int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 getHoursPart();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 getIntDaysPart();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 getMinutesPart();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 getSecondsPart();
     method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicDuration!,java.time.Duration!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicDurationByteArray();
+    method public default int toDynamicDurationByteArray(byte[]);
+    method public default int toDynamicDurationByteArray(byte[], int, int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 toIntDays();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 toIntHours();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 toIntMinutes();
@@ -157,6 +166,7 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat from(androidx.wear.protolayout.expression.DynamicDataKey<androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat!>);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromByteArray(byte[], int, int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(float);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
@@ -181,6 +191,8 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
     method public default byte[] toDynamicFloatByteArray();
+    method public default int toDynamicFloatByteArray(byte[]);
+    method public default int toDynamicFloatByteArray(byte[], int, int);
   }
 
   public static class DynamicBuilders.DynamicFloat.FloatFormatter {
@@ -202,9 +214,12 @@
   public static interface DynamicBuilders.DynamicInstant extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicDuration durationUntil(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant fromByteArray(byte[], int, int);
     method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant!,java.time.Instant!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant platformTimeWithSecondsPrecision();
     method public default byte[] toDynamicInstantByteArray();
+    method public default int toDynamicInstantByteArray(byte[]);
+    method public default int toDynamicInstantByteArray(byte[], int, int);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant withSecondsPrecision(java.time.Instant);
   }
 
@@ -227,6 +242,7 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 from(androidx.wear.protolayout.expression.DynamicDataKey<androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32!>);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromByteArray(byte[], int, int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
@@ -255,6 +271,8 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 times(int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
     method public default byte[] toDynamicInt32ByteArray();
+    method public default int toDynamicInt32ByteArray(byte[]);
+    method public default int toDynamicInt32ByteArray(byte[], int, int);
   }
 
   public static class DynamicBuilders.DynamicInt32.IntFormatter {
@@ -274,8 +292,11 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString constant(String);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString from(androidx.wear.protolayout.expression.DynamicDataKey<androidx.wear.protolayout.expression.DynamicBuilders.DynamicString!>);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString fromByteArray(byte[], int, int);
     method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicString!,java.lang.String!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicStringByteArray();
+    method public default int toDynamicStringByteArray(byte[]);
+    method public default int toDynamicStringByteArray(byte[], int, int);
   }
 
   public static interface DynamicBuilders.DynamicType {
@@ -286,10 +307,15 @@
 
   public static interface DynamicDataBuilders.DynamicDataValue {
     method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromBool(boolean);
+    method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromByteArray(byte[], int, int);
     method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromColor(@ColorInt int);
     method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromFloat(float);
     method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromInt(int);
     method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromString(String);
+    method public default byte[] toDynamicDataValueByteArray();
+    method public default int toDynamicDataValueByteArray(byte[]);
+    method public default int toDynamicDataValueByteArray(byte[], int, int);
   }
 
   public abstract class DynamicDataKey<T extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType> {
diff --git a/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
index ace5549..ba0d81d 100644
--- a/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
@@ -104,10 +104,13 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool from(androidx.wear.protolayout.expression.DynamicDataKey<androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool!>);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromByteArray(byte[], int, int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool negate();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool or(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicBoolByteArray();
+    method public default int toDynamicBoolByteArray(byte[]);
+    method public default int toDynamicBoolByteArray(byte[], int, int);
   }
 
   public static interface DynamicBuilders.DynamicColor extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
@@ -120,18 +123,24 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor constant(@ColorInt int);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor from(androidx.wear.protolayout.expression.DynamicDataKey<androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor!>);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromByteArray(byte[], int, int);
     method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor!,java.lang.Integer!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicColorByteArray();
+    method public default int toDynamicColorByteArray(byte[]);
+    method public default int toDynamicColorByteArray(byte[], int, int);
   }
 
   public static interface DynamicBuilders.DynamicDuration extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicDuration fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicDuration fromByteArray(byte[], int, int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 getHoursPart();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 getIntDaysPart();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 getMinutesPart();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 getSecondsPart();
     method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicDuration!,java.time.Duration!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicDurationByteArray();
+    method public default int toDynamicDurationByteArray(byte[]);
+    method public default int toDynamicDurationByteArray(byte[], int, int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 toIntDays();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 toIntHours();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 toIntMinutes();
@@ -157,6 +166,7 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat from(androidx.wear.protolayout.expression.DynamicDataKey<androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat!>);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromByteArray(byte[], int, int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(float);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
@@ -181,6 +191,8 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
     method public default byte[] toDynamicFloatByteArray();
+    method public default int toDynamicFloatByteArray(byte[]);
+    method public default int toDynamicFloatByteArray(byte[], int, int);
   }
 
   public static class DynamicBuilders.DynamicFloat.FloatFormatter {
@@ -202,9 +214,12 @@
   public static interface DynamicBuilders.DynamicInstant extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicDuration durationUntil(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant fromByteArray(byte[], int, int);
     method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant!,java.time.Instant!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant platformTimeWithSecondsPrecision();
     method public default byte[] toDynamicInstantByteArray();
+    method public default int toDynamicInstantByteArray(byte[]);
+    method public default int toDynamicInstantByteArray(byte[], int, int);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant withSecondsPrecision(java.time.Instant);
   }
 
@@ -227,6 +242,7 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 from(androidx.wear.protolayout.expression.DynamicDataKey<androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32!>);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromByteArray(byte[], int, int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
@@ -255,6 +271,8 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 times(int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
     method public default byte[] toDynamicInt32ByteArray();
+    method public default int toDynamicInt32ByteArray(byte[]);
+    method public default int toDynamicInt32ByteArray(byte[], int, int);
   }
 
   public static class DynamicBuilders.DynamicInt32.IntFormatter {
@@ -274,8 +292,11 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString constant(String);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString from(androidx.wear.protolayout.expression.DynamicDataKey<androidx.wear.protolayout.expression.DynamicBuilders.DynamicString!>);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString fromByteArray(byte[], int, int);
     method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicString!,java.lang.String!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicStringByteArray();
+    method public default int toDynamicStringByteArray(byte[]);
+    method public default int toDynamicStringByteArray(byte[], int, int);
   }
 
   public static interface DynamicBuilders.DynamicType {
@@ -286,10 +307,15 @@
 
   public static interface DynamicDataBuilders.DynamicDataValue {
     method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromBool(boolean);
+    method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromByteArray(byte[], int, int);
     method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromColor(@ColorInt int);
     method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromFloat(float);
     method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromInt(int);
     method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromString(String);
+    method public default byte[] toDynamicDataValueByteArray();
+    method public default int toDynamicDataValueByteArray(byte[]);
+    method public default int toDynamicDataValueByteArray(byte[], int, int);
   }
 
   public abstract class DynamicDataKey<T extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType> {
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index 7f2e9cb..1ca79e0 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -104,10 +104,13 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool eq(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool from(androidx.wear.protolayout.expression.DynamicDataKey<androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool!>);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromByteArray(byte[], int, int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool ne(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool negate();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool or(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicBoolByteArray();
+    method public default int toDynamicBoolByteArray(byte[]);
+    method public default int toDynamicBoolByteArray(byte[], int, int);
   }
 
   public static interface DynamicBuilders.DynamicColor extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
@@ -120,18 +123,24 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor constant(@ColorInt int);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor from(androidx.wear.protolayout.expression.DynamicDataKey<androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor!>);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromByteArray(byte[], int, int);
     method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor!,java.lang.Integer!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicColorByteArray();
+    method public default int toDynamicColorByteArray(byte[]);
+    method public default int toDynamicColorByteArray(byte[], int, int);
   }
 
   public static interface DynamicBuilders.DynamicDuration extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicDuration fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicDuration fromByteArray(byte[], int, int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 getHoursPart();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 getIntDaysPart();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 getMinutesPart();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 getSecondsPart();
     method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicDuration!,java.time.Duration!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicDurationByteArray();
+    method public default int toDynamicDurationByteArray(byte[]);
+    method public default int toDynamicDurationByteArray(byte[], int, int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 toIntDays();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 toIntHours();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 toIntMinutes();
@@ -157,6 +166,7 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat from(androidx.wear.protolayout.expression.DynamicDataKey<androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat!>);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat fromByteArray(byte[], int, int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(float);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat);
@@ -181,6 +191,8 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
     method public default byte[] toDynamicFloatByteArray();
+    method public default int toDynamicFloatByteArray(byte[]);
+    method public default int toDynamicFloatByteArray(byte[], int, int);
   }
 
   public static class DynamicBuilders.DynamicFloat.FloatFormatter {
@@ -202,9 +214,12 @@
   public static interface DynamicBuilders.DynamicInstant extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicDuration durationUntil(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant fromByteArray(byte[], int, int);
     method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant!,java.time.Instant!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant platformTimeWithSecondsPrecision();
     method public default byte[] toDynamicInstantByteArray();
+    method public default int toDynamicInstantByteArray(byte[]);
+    method public default int toDynamicInstantByteArray(byte[], int, int);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant withSecondsPrecision(java.time.Instant);
   }
 
@@ -227,6 +242,7 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicString format(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 from(androidx.wear.protolayout.expression.DynamicDataKey<androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32!>);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 fromByteArray(byte[], int, int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gt(int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool gte(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
@@ -255,6 +271,8 @@
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 times(int);
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat times(float);
     method public default byte[] toDynamicInt32ByteArray();
+    method public default int toDynamicInt32ByteArray(byte[]);
+    method public default int toDynamicInt32ByteArray(byte[], int, int);
   }
 
   public static class DynamicBuilders.DynamicInt32.IntFormatter {
@@ -274,8 +292,11 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString constant(String);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString from(androidx.wear.protolayout.expression.DynamicDataKey<androidx.wear.protolayout.expression.DynamicBuilders.DynamicString!>);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicString fromByteArray(byte[], int, int);
     method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicString!,java.lang.String!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicStringByteArray();
+    method public default int toDynamicStringByteArray(byte[]);
+    method public default int toDynamicStringByteArray(byte[], int, int);
   }
 
   public static interface DynamicBuilders.DynamicType {
@@ -286,10 +307,15 @@
 
   public static interface DynamicDataBuilders.DynamicDataValue {
     method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromBool(boolean);
+    method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromByteArray(byte[]);
+    method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromByteArray(byte[], int, int);
     method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromColor(@ColorInt int);
     method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromFloat(float);
     method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromInt(int);
     method public static androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue fromString(String);
+    method public default byte[] toDynamicDataValueByteArray();
+    method public default int toDynamicDataValueByteArray(byte[]);
+    method public default int toDynamicDataValueByteArray(byte[], int, int);
   }
 
   public abstract class DynamicDataKey<T extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType> {
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
index 162b840..284104c 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
@@ -38,9 +38,11 @@
 import androidx.wear.protolayout.expression.FixedValueBuilders.FixedString;
 import androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue;
 import androidx.wear.protolayout.expression.proto.DynamicProto;
+import androidx.wear.protolayout.protobuf.CodedInputStream;
+import androidx.wear.protolayout.protobuf.CodedOutputStream;
 import androidx.wear.protolayout.protobuf.ExtensionRegistryLite;
-import androidx.wear.protolayout.protobuf.InvalidProtocolBufferException;
 
+import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.time.Duration;
@@ -1506,25 +1508,72 @@
         /**
          * Creates a {@link DynamicInt32} from a byte array generated by {@link
          * #toDynamicInt32ByteArray()}.
+         *
+         * @throws IllegalArgumentException if the byte array does not contain a valid serialization
          */
         @NonNull
         static DynamicInt32 fromByteArray(@NonNull byte[] byteArray) {
+            return fromByteArray(byteArray, 0, byteArray.length);
+        }
+
+        /**
+         * Creates a {@link DynamicInt32} from the provided byte array at the provided offset and
+         * length, that was generated by one of the {@link #toDynamicInt32ByteArray} overloads.
+         *
+         * @throws IllegalArgumentException if the byte array does not contain a valid serialization
+         *     in the provided offset and length
+         */
+        @NonNull
+        static DynamicInt32 fromByteArray(@NonNull byte[] byteArray, int offset, int length) {
             try {
                 return dynamicInt32FromProto(
                         DynamicProto.DynamicInt32.parseFrom(
-                                byteArray, ExtensionRegistryLite.getEmptyRegistry()));
-            } catch (InvalidProtocolBufferException e) {
+                                CodedInputStream.newInstance(byteArray, offset, length),
+                                ExtensionRegistryLite.getEmptyRegistry()));
+            } catch (IOException e) {
                 throw new IllegalArgumentException(
                         "Byte array could not be parsed into DynamicInt32", e);
             }
         }
 
-        /** Creates a byte array that can later be used with {@link #fromByteArray(byte[])}. */
+        /**
+         * Serializes the {@link DynamicInt32} into a new byte array that can later be used with
+         * {@link #fromByteArray(byte[])}.
+         */
         @NonNull
         default byte[] toDynamicInt32ByteArray() {
             return toDynamicInt32Proto().toByteArray();
         }
 
+        /**
+         * Serializes the {@link DynamicInt32} into the provided byte array, returning the amount of
+         * bytes written, that can later be used with {@code DynamicInt32.fromByteArray(byteArray,
+         * 0, bytesWritten)}.
+         *
+         * @throws IllegalArgumentException if the byte array is too small
+         */
+        default int toDynamicInt32ByteArray(@NonNull byte[] byteArray) {
+            return toDynamicInt32ByteArray(byteArray, 0, byteArray.length);
+        }
+
+        /**
+         * Serializes the {@link DynamicInt32} into the provided byte array, returning the amount of
+         * bytes written, limited by the provided offset and length, that can later be used with
+         * {@code DynamicInt32.fromByteArray(byteArray, offset, bytesWritten)}.
+         *
+         * @throws IllegalArgumentException if the byte array is too small
+         */
+        default int toDynamicInt32ByteArray(@NonNull byte[] byteArray, int offset, int length) {
+            CodedOutputStream stream = CodedOutputStream.newInstance(byteArray, offset, length);
+            try {
+                toDynamicInt32Proto().writeTo(stream);
+            } catch (IOException e) {
+                throw new IllegalArgumentException(
+                        "Provided byte array not large enough to contain this DynamicInt32", e);
+            }
+            return stream.getTotalBytesWritten();
+        }
+
         /** Creates a constant-valued {@link DynamicInt32}. */
         @NonNull
         static DynamicInt32 constant(int constant) {
@@ -3283,26 +3332,73 @@
         /**
          * Creates a {@link DynamicString} from a byte array generated by {@link
          * #toDynamicStringByteArray()}.
+         *
+         * @throws IllegalArgumentException if the byte array does not contain a valid serialization
          */
         @NonNull
         static DynamicString fromByteArray(@NonNull byte[] byteArray) {
+            return fromByteArray(byteArray, 0, byteArray.length);
+        }
+
+        /**
+         * Creates a {@link DynamicString} from the provided byte array at the provided offset and
+         * length, that was generated by one of the {@link #toDynamicStringByteArray} overloads.
+         *
+         * @throws IllegalArgumentException if the byte array does not contain a valid serialization
+         *     in the provided offset and length
+         */
+        @NonNull
+        static DynamicString fromByteArray(@NonNull byte[] byteArray, int offset, int length) {
             try {
                 return dynamicStringFromProto(
                         DynamicProto.DynamicString.parseFrom(
-                                byteArray, ExtensionRegistryLite.getEmptyRegistry()));
-            } catch (InvalidProtocolBufferException e) {
+                                CodedInputStream.newInstance(byteArray, offset, length),
+                                ExtensionRegistryLite.getEmptyRegistry()));
+            } catch (IOException e) {
                 throw new IllegalArgumentException(
                         "Byte array could not be parsed into DynamicString", e);
             }
         }
 
-        /** Creates a byte array that can later be used with {@link #fromByteArray(byte[])}. */
+        /**
+         * Serializes the {@link DynamicString} into a new byte array that can later be used with
+         * {@link #fromByteArray(byte[])}.
+         */
         @NonNull
         default byte[] toDynamicStringByteArray() {
             return toDynamicStringProto().toByteArray();
         }
 
         /**
+         * Serializes the {@link DynamicString} into the provided byte array, returning the amount
+         * of bytes written, that can later be used with {@code DynamicString.fromByteArray(
+         * byteArray, 0, bytesWritten)}.
+         *
+         * @throws IllegalArgumentException if the byte array is too small
+         */
+        default int toDynamicStringByteArray(@NonNull byte[] byteArray) {
+            return toDynamicStringByteArray(byteArray, 0, byteArray.length);
+        }
+
+        /**
+         * Serializes the {@link DynamicString} into the provided byte array, returning the amount
+         * of bytes written, limited by the provided offset and length, that can later be used with
+         * {@code DynamicString.fromByteArray(byteArray, offset, bytesWritten)}.
+         *
+         * @throws IllegalArgumentException if the byte array is too small
+         */
+        default int toDynamicStringByteArray(@NonNull byte[] byteArray, int offset, int length) {
+            CodedOutputStream stream = CodedOutputStream.newInstance(byteArray, offset, length);
+            try {
+                toDynamicStringProto().writeTo(stream);
+            } catch (IOException e) {
+                throw new IllegalArgumentException(
+                        "Provided byte array not large enough to contain this DynamicString", e);
+            }
+            return stream.getTotalBytesWritten();
+        }
+
+        /**
          * Creates a constant-valued {@link DynamicString}. The resulted {@link DynamicString} is
          * subject to being truncated if it's too long.
          */
@@ -4081,26 +4177,73 @@
         /**
          * Creates a {@link DynamicFloat} from a byte array generated by {@link
          * #toDynamicFloatByteArray()}.
+         *
+         * @throws IllegalArgumentException if the byte array does not contain a valid serialization
          */
         @NonNull
         static DynamicFloat fromByteArray(@NonNull byte[] byteArray) {
+            return fromByteArray(byteArray, 0, byteArray.length);
+        }
+
+        /**
+         * Creates a {@link DynamicFloat} from the provided byte array at the provided offset and
+         * length, that was generated by one of the {@link #toDynamicFloatByteArray} overloads.
+         *
+         * @throws IllegalArgumentException if the byte array does not contain a valid serialization
+         *     in the provided offset and length
+         */
+        @NonNull
+        static DynamicFloat fromByteArray(@NonNull byte[] byteArray, int offset, int length) {
             try {
                 return dynamicFloatFromProto(
                         DynamicProto.DynamicFloat.parseFrom(
-                                byteArray, ExtensionRegistryLite.getEmptyRegistry()));
-            } catch (InvalidProtocolBufferException e) {
+                                CodedInputStream.newInstance(byteArray, offset, length),
+                                ExtensionRegistryLite.getEmptyRegistry()));
+            } catch (IOException e) {
                 throw new IllegalArgumentException(
                         "Byte array could not be parsed into DynamicFloat", e);
             }
         }
 
-        /** Creates a byte array that can later be used with {@link #fromByteArray(byte[])}. */
+        /**
+         * Serializes the {@link DynamicFloat} into a new byte array that can later be used with
+         * {@link #fromByteArray(byte[])}.
+         */
         @NonNull
         default byte[] toDynamicFloatByteArray() {
             return toDynamicFloatProto().toByteArray();
         }
 
         /**
+         * Serializes the {@link DynamicFloat} into the provided byte array, returning the amount of
+         * bytes written, that can later be used with {@code DynamicFloat.fromByteArray(byteArray,
+         * 0, bytesWritten)}.
+         *
+         * @throws IllegalArgumentException if the byte array is too small
+         */
+        default int toDynamicFloatByteArray(@NonNull byte[] byteArray) {
+            return toDynamicFloatByteArray(byteArray, 0, byteArray.length);
+        }
+
+        /**
+         * Serializes the {@link DynamicFloat} into the provided byte array, returning the amount of
+         * bytes written, limited by the provided offset and length, that can later be used with
+         * {@code DynamicFloat.fromByteArray(byteArray, offset, bytesWritten)}.
+         *
+         * @throws IllegalArgumentException if the byte array is too small
+         */
+        default int toDynamicFloatByteArray(@NonNull byte[] byteArray, int offset, int length) {
+            CodedOutputStream stream = CodedOutputStream.newInstance(byteArray, offset, length);
+            try {
+                toDynamicFloatProto().writeTo(stream);
+            } catch (IOException e) {
+                throw new IllegalArgumentException(
+                        "Provided byte array not large enough to contain this DynamicFloat", e);
+            }
+            return stream.getTotalBytesWritten();
+        }
+
+        /**
          * Creates a constant-valued {@link DynamicFloat}.
          *
          * <p>If {@code Float.isNan(constant)} is true, the value will be invalid. And any
@@ -5668,25 +5811,72 @@
         /**
          * Creates a {@link DynamicBool} from a byte array generated by {@link
          * #toDynamicBoolByteArray()}.
+         *
+         * @throws IllegalArgumentException if the byte array does not contain a valid serialization
          */
         @NonNull
         static DynamicBool fromByteArray(@NonNull byte[] byteArray) {
+            return fromByteArray(byteArray, 0, byteArray.length);
+        }
+
+        /**
+         * Creates a {@link DynamicBool} from the provided byte array at the provided offset and
+         * length, that was generated by one of the {@link #toDynamicBoolByteArray} overloads.
+         *
+         * @throws IllegalArgumentException if the byte array does not contain a valid serialization
+         *     in the provided offset and length
+         */
+        @NonNull
+        static DynamicBool fromByteArray(@NonNull byte[] byteArray, int offset, int length) {
             try {
                 return dynamicBoolFromProto(
                         DynamicProto.DynamicBool.parseFrom(
-                                byteArray, ExtensionRegistryLite.getEmptyRegistry()));
-            } catch (InvalidProtocolBufferException e) {
+                                CodedInputStream.newInstance(byteArray, offset, length),
+                                ExtensionRegistryLite.getEmptyRegistry()));
+            } catch (IOException e) {
                 throw new IllegalArgumentException(
                         "Byte array could not be parsed into DynamicBool", e);
             }
         }
 
-        /** Creates a byte array that can later be used with {@link #fromByteArray(byte[])}. */
+        /**
+         * Serializes the {@link DynamicBool} into a new byte array that can later be used with
+         * {@link #fromByteArray(byte[])}.
+         */
         @NonNull
         default byte[] toDynamicBoolByteArray() {
             return toDynamicBoolProto().toByteArray();
         }
 
+        /**
+         * Serializes the {@link DynamicBool} into the provided byte array, returning the amount of
+         * bytes written, that can later be used with {@code DynamicBool.fromByteArray(byteArray, 0,
+         * bytesWritten)}.
+         *
+         * @throws IllegalArgumentException if the byte array is too small
+         */
+        default int toDynamicBoolByteArray(@NonNull byte[] byteArray) {
+            return toDynamicBoolByteArray(byteArray, 0, byteArray.length);
+        }
+
+        /**
+         * Serializes the {@link DynamicBool} into the provided byte array, returning the amount of
+         * bytes written, limited by the provided offset and length, that can later be used with
+         * {@code DynamicBool.fromByteArray(byteArray, offset, bytesWritten)}.
+         *
+         * @throws IllegalArgumentException if the byte array is too small
+         */
+        default int toDynamicBoolByteArray(@NonNull byte[] byteArray, int offset, int length) {
+            CodedOutputStream stream = CodedOutputStream.newInstance(byteArray, offset, length);
+            try {
+                toDynamicBoolProto().writeTo(stream);
+            } catch (IOException e) {
+                throw new IllegalArgumentException(
+                        "Provided byte array not large enough to contain this DynamicBool", e);
+            }
+            return stream.getTotalBytesWritten();
+        }
+
         /** Creates a constant-valued {@link DynamicBool}. */
         @NonNull
         static DynamicBool constant(boolean constant) {
@@ -6401,25 +6591,72 @@
         /**
          * Creates a {@link DynamicColor} from a byte array generated by {@link
          * #toDynamicColorByteArray()}.
+         *
+         * @throws IllegalArgumentException if the byte array does not contain a valid serialization
          */
         @NonNull
         static DynamicColor fromByteArray(@NonNull byte[] byteArray) {
+            return fromByteArray(byteArray, 0, byteArray.length);
+        }
+
+        /**
+         * Creates a {@link DynamicColor} from the provided byte array at the provided offset and
+         * length, that was generated by one of the {@link #toDynamicColorByteArray} overloads.
+         *
+         * @throws IllegalArgumentException if the byte array does not contain a valid serialization
+         *     in the provided offset and length
+         */
+        @NonNull
+        static DynamicColor fromByteArray(@NonNull byte[] byteArray, int offset, int length) {
             try {
                 return dynamicColorFromProto(
                         DynamicProto.DynamicColor.parseFrom(
-                                byteArray, ExtensionRegistryLite.getEmptyRegistry()));
-            } catch (InvalidProtocolBufferException e) {
+                                CodedInputStream.newInstance(byteArray, offset, length),
+                                ExtensionRegistryLite.getEmptyRegistry()));
+            } catch (IOException e) {
                 throw new IllegalArgumentException(
                         "Byte array could not be parsed into DynamicColor", e);
             }
         }
 
-        /** Creates a byte array that can later be used with {@link #fromByteArray(byte[])}. */
+        /**
+         * Serializes the {@link DynamicColor} into a new byte array that can later be used with
+         * {@link #fromByteArray(byte[])}.
+         */
         @NonNull
         default byte[] toDynamicColorByteArray() {
             return toDynamicColorProto().toByteArray();
         }
 
+        /**
+         * Serializes the {@link DynamicColor} into the provided byte array, returning the amount of
+         * bytes written, that can later be used with {@code DynamicColor.fromByteArray(byteArray,
+         * 0, bytesWritten)}.
+         *
+         * @throws IllegalArgumentException if the byte array is too small
+         */
+        default int toDynamicColorByteArray(@NonNull byte[] byteArray) {
+            return toDynamicColorByteArray(byteArray, 0, byteArray.length);
+        }
+
+        /**
+         * Serializes the {@link DynamicColor} into the provided byte array, returning the amount of
+         * bytes written, limited by the provided offset and length, that can later be used with
+         * {@code DynamicColor.fromByteArray(byteArray, offset, bytesWritten)}.
+         *
+         * @throws IllegalArgumentException if the byte array is too small
+         */
+        default int toDynamicColorByteArray(@NonNull byte[] byteArray, int offset, int length) {
+            CodedOutputStream stream = CodedOutputStream.newInstance(byteArray, offset, length);
+            try {
+                toDynamicColorProto().writeTo(stream);
+            } catch (IOException e) {
+                throw new IllegalArgumentException(
+                        "Provided byte array not large enough to contain this DynamicColor", e);
+            }
+            return stream.getTotalBytesWritten();
+        }
+
         /** Creates a constant-valued {@link DynamicColor}. */
         @NonNull
         static DynamicColor constant(@ColorInt int constant) {
@@ -6840,26 +7077,73 @@
         /**
          * Creates a {@link DynamicInstant} from a byte array generated by {@link
          * #toDynamicInstantByteArray()}.
+         *
+         * @throws IllegalArgumentException if the byte array does not contain a valid serialization
          */
         @NonNull
         static DynamicInstant fromByteArray(@NonNull byte[] byteArray) {
+            return fromByteArray(byteArray, 0, byteArray.length);
+        }
+
+        /**
+         * Creates a {@link DynamicInstant} from the provided byte array at the provided offset and
+         * length, that was generated by one of the {@link #toDynamicInstantByteArray} overloads.
+         *
+         * @throws IllegalArgumentException if the byte array does not contain a valid serialization
+         *     in the provided offset and length
+         */
+        @NonNull
+        static DynamicInstant fromByteArray(@NonNull byte[] byteArray, int offset, int length) {
             try {
                 return dynamicInstantFromProto(
                         DynamicProto.DynamicInstant.parseFrom(
-                                byteArray, ExtensionRegistryLite.getEmptyRegistry()));
-            } catch (InvalidProtocolBufferException e) {
+                                CodedInputStream.newInstance(byteArray, offset, length),
+                                ExtensionRegistryLite.getEmptyRegistry()));
+            } catch (IOException e) {
                 throw new IllegalArgumentException(
                         "Byte array could not be parsed into DynamicInstant", e);
             }
         }
 
-        /** Creates a byte array that can later be used with {@link #fromByteArray(byte[])}. */
+        /**
+         * Serializes the {@link DynamicInstant} into a new byte array that can later be used with
+         * {@link #fromByteArray(byte[])}.
+         */
         @NonNull
         default byte[] toDynamicInstantByteArray() {
             return toDynamicInstantProto().toByteArray();
         }
 
         /**
+         * Serializes the {@link DynamicInstant} into the provided byte array, returning the amount
+         * of bytes written, that can later be used with {@code DynamicInstant.fromByteArray(
+         * byteArray, 0, bytesWritten)}.
+         *
+         * @throws IllegalArgumentException if the byte array is too small
+         */
+        default int toDynamicInstantByteArray(@NonNull byte[] byteArray) {
+            return toDynamicInstantByteArray(byteArray, 0, byteArray.length);
+        }
+
+        /**
+         * Serializes the {@link DynamicInstant} into the provided byte array, returning the amount
+         * of bytes written, limited by the provided offset and length, that can later be used with
+         * {@code DynamicInstant.fromByteArray(byteArray, offset, bytesWritten)}.
+         *
+         * @throws IllegalArgumentException if the byte array is too small
+         */
+        default int toDynamicInstantByteArray(@NonNull byte[] byteArray, int offset, int length) {
+            CodedOutputStream stream = CodedOutputStream.newInstance(byteArray, offset, length);
+            try {
+                toDynamicInstantProto().writeTo(stream);
+            } catch (IOException e) {
+                throw new IllegalArgumentException(
+                        "Provided byte array not large enough to contain this DynamicInstant", e);
+            }
+            return stream.getTotalBytesWritten();
+        }
+
+        /**
          * Creates a constant-valued {@link DynamicInstant} from an {@link Instant}. If {@link
          * Instant} precision is greater than seconds, then any excess precision information will be
          * dropped.
@@ -7266,26 +7550,73 @@
         /**
          * Creates a {@link DynamicDuration} from a byte array generated by {@link
          * #toDynamicDurationByteArray()}.
+         *
+         * @throws IllegalArgumentException if the byte array does not contain a valid serialization
          */
         @NonNull
         static DynamicDuration fromByteArray(@NonNull byte[] byteArray) {
+            return fromByteArray(byteArray, 0, byteArray.length);
+        }
+
+        /**
+         * Creates a {@link DynamicDuration} from the provided byte array at the provided offset and
+         * length, that was generated by one of the {@link #toDynamicDurationByteArray} overloads.
+         *
+         * @throws IllegalArgumentException if the byte array does not contain a valid serialization
+         *     in the provided offset and length
+         */
+        @NonNull
+        static DynamicDuration fromByteArray(@NonNull byte[] byteArray, int offset, int length) {
             try {
                 return dynamicDurationFromProto(
                         DynamicProto.DynamicDuration.parseFrom(
-                                byteArray, ExtensionRegistryLite.getEmptyRegistry()));
-            } catch (InvalidProtocolBufferException e) {
+                                CodedInputStream.newInstance(byteArray, offset, length),
+                                ExtensionRegistryLite.getEmptyRegistry()));
+            } catch (IOException e) {
                 throw new IllegalArgumentException(
                         "Byte array could not be parsed into DynamicDuration", e);
             }
         }
 
-        /** Creates a byte array that can later be used with {@link #fromByteArray(byte[])}. */
+        /**
+         * Serializes the {@link DynamicDuration} into a new byte array that can later be used with
+         * {@link #fromByteArray(byte[])}.
+         */
         @NonNull
         default byte[] toDynamicDurationByteArray() {
             return toDynamicDurationProto().toByteArray();
         }
 
         /**
+         * Serializes the {@link DynamicDuration} into the provided byte array, returning the amount
+         * of bytes written, that can later be used with {@code DynamicDuration.fromByteArray(
+         * byteArray, 0, bytesWritten)}.
+         *
+         * @throws IllegalArgumentException if the byte array is too small
+         */
+        default int toDynamicDurationByteArray(@NonNull byte[] byteArray) {
+            return toDynamicDurationByteArray(byteArray, 0, byteArray.length);
+        }
+
+        /**
+         * Serializes the {@link DynamicDuration} into the provided byte array, returning the amount
+         * of bytes written, limited by the provided offset and length, that can later be used with
+         * {@code DynamicDuration.fromByteArray(byteArray, offset, bytesWritten)}.
+         *
+         * @throws IllegalArgumentException if the byte array is too small
+         */
+        default int toDynamicDurationByteArray(@NonNull byte[] byteArray, int offset, int length) {
+            CodedOutputStream stream = CodedOutputStream.newInstance(byteArray, offset, length);
+            try {
+                toDynamicDurationProto().writeTo(stream);
+            } catch (IOException e) {
+                throw new IllegalArgumentException(
+                        "Provided byte array not large enough to contain this DynamicDuration", e);
+            }
+            return stream.getTotalBytesWritten();
+        }
+
+        /**
          * Creates a constant-valued {@link DynamicDuration} from a {@link Duration}. If {@link
          * Duration} precision is greater than seconds, then any excess precision information will
          * be dropped.
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java
index 7f4eddd..8f91189 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java
@@ -27,6 +27,11 @@
 import androidx.wear.protolayout.expression.FixedValueBuilders.FixedInt32;
 import androidx.wear.protolayout.expression.FixedValueBuilders.FixedString;
 import androidx.wear.protolayout.expression.proto.DynamicDataProto;
+import androidx.wear.protolayout.protobuf.CodedInputStream;
+import androidx.wear.protolayout.protobuf.CodedOutputStream;
+import androidx.wear.protolayout.protobuf.ExtensionRegistryLite;
+
+import java.io.IOException;
 
 /** Builders for dynamic data value of a provider. */
 public final class DynamicDataBuilders {
@@ -46,6 +51,75 @@
     @NonNull
     DynamicDataProto.DynamicDataValue toDynamicDataValueProto();
 
+    /**
+     * Creates a {@link DynamicDataValue} from a byte array generated by {@link
+     * #toDynamicDataValueByteArray()}.
+     *
+     * @throws IllegalArgumentException if the byte array does not contain a valid serialization
+     */
+    @NonNull
+    static DynamicDataValue fromByteArray(@NonNull byte[] byteArray) {
+      return fromByteArray(byteArray, 0, byteArray.length);
+    }
+
+    /**
+     * Creates a {@link DynamicDataValue} from the provided byte array at the provided offset and
+     * length, that was generated by one of the {@link #toDynamicDataValueByteArray} overloads.
+     *
+     * @throws IllegalArgumentException if the byte array does not contain a valid serialization in
+     *     the provided offset and length
+     */
+    @NonNull
+    static DynamicDataValue fromByteArray(@NonNull byte[] byteArray, int offset, int length) {
+      try {
+        return dynamicDataValueFromProto(
+                DynamicDataProto.DynamicDataValue.parseFrom(
+                        CodedInputStream.newInstance(byteArray, offset, length),
+                        ExtensionRegistryLite.getEmptyRegistry()));
+      } catch (IOException e) {
+        throw new IllegalArgumentException(
+                "Byte array could not be parsed into DynamicDataValue", e);
+      }
+    }
+
+    /**
+     * Serializes the {@link DynamicDataValue} into a new byte array that can later be used with
+     * {@link #fromByteArray(byte[])}.
+     */
+    @NonNull
+    default byte[] toDynamicDataValueByteArray() {
+      return toDynamicDataValueProto().toByteArray();
+    }
+
+    /**
+     * Serializes the {@link DynamicDataValue} into the provided byte array, returning the amount of
+     * bytes written, that can later be used with {@code DynamicDataValue.fromByteArray(byteArray,
+     * 0, bytesWritten)}.
+     *
+     * @throws IllegalArgumentException if the byte array is too small
+     */
+    default int toDynamicDataValueByteArray(@NonNull byte[] byteArray) {
+      return toDynamicDataValueByteArray(byteArray, 0, byteArray.length);
+    }
+
+    /**
+     * Serializes the {@link DynamicDataValue} into the provided byte array, returning the amount of
+     * bytes written, limited by the provided offset and length, that can later be used with {@code
+     * DynamicDataValue.fromByteArray(byteArray, offset, bytesWritten)}.
+     *
+     * @throws IllegalArgumentException if the byte array is too small
+     */
+    default int toDynamicDataValueByteArray(@NonNull byte[] byteArray, int offset, int length) {
+      CodedOutputStream stream = CodedOutputStream.newInstance(byteArray, offset, length);
+      try {
+        toDynamicDataValueProto().writeTo(stream);
+      } catch (IOException e) {
+        throw new IllegalArgumentException(
+                "Provided byte array not large enough to contain this DynamicDataValue", e);
+      }
+      return stream.getTotalBytesWritten();
+    }
+
     /** Creates a boolean {@link DynamicDataValue}. */
     @NonNull
     static DynamicDataValue fromBool(boolean constant) {
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicBoolTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicBoolTest.java
index c7263ec..3c0bc87 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicBoolTest.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicBoolTest.java
@@ -144,7 +144,7 @@
     }
 
     @Test
-    public void validProto() {
+    public void fromByteArray_validProto() {
         DynamicBool from = DynamicBool.constant(true);
         DynamicBool to = DynamicBool.fromByteArray(from.toDynamicBoolByteArray());
 
@@ -152,8 +152,56 @@
     }
 
     @Test
-    public void invalidProto() {
+    public void fromByteArray_invalidProto() {
         assertThrows(
                 IllegalArgumentException.class, () -> DynamicBool.fromByteArray(new byte[] {1}));
     }
+
+    @Test
+    public void fromByteArray_existingByteArray() {
+        DynamicBool from = DynamicBool.constant(true);
+        byte[] buffer = new byte[100];
+        int written = from.toDynamicBoolByteArray(buffer, 10, 50);
+
+        DynamicBool to = DynamicBool.fromByteArray(buffer, 10, written);
+
+        assertThat(to.toDynamicBoolProto().getFixed().getValue()).isTrue();
+    }
+
+    @Test
+    public void fromByteArray_existingByteArrayTooSmall() {
+        DynamicBool from = DynamicBool.constant(true);
+        byte[] buffer = new byte[100];
+        int written = from.toDynamicBoolByteArray(buffer);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> DynamicBool.fromByteArray(buffer, 0, written - 1));
+    }
+
+    @Test
+    public void fromByteArray_existingByteArrayTooLarge() {
+        DynamicBool from = DynamicBool.constant(true);
+        byte[] buffer = new byte[100];
+        int written = from.toDynamicBoolByteArray(buffer);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> DynamicBool.fromByteArray(buffer, 0, written + 1));
+    }
+
+    @Test
+    public void toByteArray_existingByteArrayTooSmall() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> DynamicBool.constant(true).toDynamicBoolByteArray(new byte[1]));
+    }
+
+    @Test
+    public void toByteArray_existingByteArraySameSize() {
+        DynamicBool from = DynamicBool.constant(true);
+
+        assertThat(from.toDynamicBoolByteArray(new byte[100]))
+                .isEqualTo(from.toDynamicBoolByteArray().length);
+    }
 }
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicColorTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicColorTest.java
index 2751e61..d879ce5f 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicColorTest.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicColorTest.java
@@ -140,7 +140,7 @@
   }
 
   @Test
-  public void validProto() {
+  public void fromByteArray_validProto() {
     DynamicColor from = DynamicColor.constant(CONSTANT_VALUE);
     DynamicColor to = DynamicColor.fromByteArray(from.toDynamicColorByteArray());
 
@@ -148,7 +148,55 @@
   }
 
   @Test
-  public void invalidProto() {
+  public void fromByteArray_invalidProto() {
     assertThrows(IllegalArgumentException.class, () -> DynamicColor.fromByteArray(new byte[] {1}));
   }
+
+  @Test
+  public void fromByteArray_existingByteArray() {
+    DynamicColor from = DynamicColor.constant(CONSTANT_VALUE);
+    byte[] buffer = new byte[100];
+    int written = from.toDynamicColorByteArray(buffer, 10, 50);
+
+    DynamicColor to = DynamicColor.fromByteArray(buffer, 10, written);
+
+    assertThat(to.toDynamicColorProto().getFixed().getArgb()).isEqualTo(CONSTANT_VALUE);
+  }
+
+  @Test
+  public void fromByteArray_existingByteArrayTooSmall() {
+    DynamicColor from = DynamicColor.constant(CONSTANT_VALUE);
+    byte[] buffer = new byte[100];
+    int written = from.toDynamicColorByteArray(buffer);
+
+    assertThrows(
+            IllegalArgumentException.class,
+            () -> DynamicColor.fromByteArray(buffer, 0, written - 1));
+  }
+
+  @Test
+  public void fromByteArray_existingByteArrayTooLarge() {
+    DynamicColor from = DynamicColor.constant(CONSTANT_VALUE);
+    byte[] buffer = new byte[100];
+    int written = from.toDynamicColorByteArray(buffer);
+
+    assertThrows(
+            IllegalArgumentException.class,
+            () -> DynamicColor.fromByteArray(buffer, 0, written + 1));
+  }
+
+  @Test
+  public void toByteArray_existingByteArrayTooSmall() {
+    assertThrows(
+            IllegalArgumentException.class,
+            () -> DynamicColor.constant(CONSTANT_VALUE).toDynamicColorByteArray(new byte[1]));
+  }
+
+  @Test
+  public void toByteArray_existingByteArraySameSize() {
+    DynamicColor from = DynamicColor.constant(CONSTANT_VALUE);
+
+    assertThat(from.toDynamicColorByteArray(new byte[100]))
+            .isEqualTo(from.toDynamicColorByteArray().length);
+  }
 }
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicFloatTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicFloatTest.java
index 3036fbed..c7b9e7e 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicFloatTest.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicFloatTest.java
@@ -238,7 +238,7 @@
     }
 
     @Test
-    public void validProto() {
+    public void fromByteArray_validProto() {
         DynamicFloat from = DynamicFloat.constant(CONSTANT_VALUE);
         DynamicFloat to = DynamicFloat.fromByteArray(from.toDynamicFloatByteArray());
 
@@ -246,8 +246,56 @@
     }
 
     @Test
-    public void invalidProto() {
+    public void fromByteArray_invalidProto() {
         assertThrows(
                 IllegalArgumentException.class, () -> DynamicFloat.fromByteArray(new byte[] {1}));
     }
+
+    @Test
+    public void fromByteArray_existingByteArray() {
+        DynamicFloat from = DynamicFloat.constant(CONSTANT_VALUE);
+        byte[] buffer = new byte[100];
+        int written = from.toDynamicFloatByteArray(buffer, 10, 50);
+
+        DynamicFloat to = DynamicFloat.fromByteArray(buffer, 10, written);
+
+        assertThat(to.toDynamicFloatProto().getFixed().getValue()).isEqualTo(CONSTANT_VALUE);
+    }
+
+    @Test
+    public void fromByteArray_existingByteArrayTooSmall() {
+        DynamicFloat from = DynamicFloat.constant(CONSTANT_VALUE);
+        byte[] buffer = new byte[100];
+        int written = from.toDynamicFloatByteArray(buffer);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> DynamicFloat.fromByteArray(buffer, 0, written - 1));
+    }
+
+    @Test
+    public void fromByteArray_existingByteArrayTooLarge() {
+        DynamicFloat from = DynamicFloat.constant(CONSTANT_VALUE);
+        byte[] buffer = new byte[100];
+        int written = from.toDynamicFloatByteArray(buffer);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> DynamicFloat.fromByteArray(buffer, 0, written + 1));
+    }
+
+    @Test
+    public void toByteArray_existingByteArrayTooSmall() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> DynamicFloat.constant(CONSTANT_VALUE).toDynamicFloatByteArray(new byte[1]));
+    }
+
+    @Test
+    public void toByteArray_existingByteArraySameSize() {
+        DynamicFloat from = DynamicFloat.constant(CONSTANT_VALUE);
+
+        assertThat(from.toDynamicFloatByteArray(new byte[100]))
+                .isEqualTo(from.toDynamicFloatByteArray().length);
+    }
 }
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java
index a130199..ff1bc0f 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java
@@ -131,7 +131,7 @@
     }
 
     @Test
-    public void validProto() {
+    public void fromByteArray_validProto() {
         DynamicInt32 from = DynamicInt32.constant(CONSTANT_VALUE);
         DynamicInt32 to = DynamicInt32.fromByteArray(from.toDynamicInt32ByteArray());
 
@@ -139,8 +139,56 @@
     }
 
     @Test
-    public void invalidProto() {
+    public void fromByteArray_invalidProto() {
         assertThrows(
                 IllegalArgumentException.class, () -> DynamicInt32.fromByteArray(new byte[] {1}));
     }
+
+    @Test
+    public void fromByteArray_existingByteArray() {
+        DynamicInt32 from = DynamicInt32.constant(CONSTANT_VALUE);
+        byte[] buffer = new byte[100];
+        int written = from.toDynamicInt32ByteArray(buffer, 10, 50);
+
+        DynamicInt32 to = DynamicInt32.fromByteArray(buffer, 10, written);
+
+        assertThat(to.toDynamicInt32Proto().getFixed().getValue()).isEqualTo(CONSTANT_VALUE);
+    }
+
+    @Test
+    public void fromByteArray_existingByteArrayTooSmall() {
+        DynamicInt32 from = DynamicInt32.constant(CONSTANT_VALUE);
+        byte[] buffer = new byte[100];
+        int written = from.toDynamicInt32ByteArray(buffer);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> DynamicInt32.fromByteArray(buffer, 0, written - 1));
+    }
+
+    @Test
+    public void fromByteArray_existingByteArrayTooLarge() {
+        DynamicInt32 from = DynamicInt32.constant(CONSTANT_VALUE);
+        byte[] buffer = new byte[100];
+        int written = from.toDynamicInt32ByteArray(buffer);
+
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> DynamicInt32.fromByteArray(buffer, 0, written + 1));
+    }
+
+    @Test
+    public void toByteArray_existingByteArrayTooSmall() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> DynamicInt32.constant(CONSTANT_VALUE).toDynamicInt32ByteArray(new byte[1]));
+    }
+
+    @Test
+    public void toByteArray_existingByteArraySameSize() {
+        DynamicInt32 from = DynamicInt32.constant(CONSTANT_VALUE);
+
+        assertThat(from.toDynamicInt32ByteArray(new byte[100]))
+                .isEqualTo(from.toDynamicInt32ByteArray().length);
+    }
 }
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicStringTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicStringTest.java
index b33df22..658664f 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicStringTest.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicStringTest.java
@@ -120,7 +120,7 @@
   }
 
   @Test
-  public void validProto() {
+  public void fromByteArray_validProto() {
     DynamicString from = DynamicString.constant(CONSTANT_VALUE);
     DynamicString to = DynamicString.fromByteArray(from.toDynamicStringByteArray());
 
@@ -128,7 +128,55 @@
   }
 
   @Test
-  public void invalidProto() {
+  public void fromByteArray_invalidProto() {
     assertThrows(IllegalArgumentException.class, () -> DynamicString.fromByteArray(new byte[] {1}));
   }
+
+  @Test
+  public void fromByteArray_existingByteArray() {
+    DynamicString from = DynamicString.constant(CONSTANT_VALUE);
+    byte[] buffer = new byte[100];
+    int written = from.toDynamicStringByteArray(buffer, 10, 50);
+
+    DynamicString to = DynamicString.fromByteArray(buffer, 10, written);
+
+    assertThat(to.toDynamicStringProto().getFixed().getValue()).isEqualTo(CONSTANT_VALUE);
+  }
+
+  @Test
+  public void fromByteArray_existingByteArrayTooSmall() {
+    DynamicString from = DynamicString.constant(CONSTANT_VALUE);
+    byte[] buffer = new byte[100];
+    int written = from.toDynamicStringByteArray(buffer);
+
+    assertThrows(
+            IllegalArgumentException.class,
+            () -> DynamicString.fromByteArray(buffer, 0, written - 1));
+  }
+
+  @Test
+  public void fromByteArray_existingByteArrayTooLarge() {
+    DynamicString from = DynamicString.constant(CONSTANT_VALUE);
+    byte[] buffer = new byte[100];
+    int written = from.toDynamicStringByteArray(buffer);
+
+    assertThrows(
+            IllegalArgumentException.class,
+            () -> DynamicString.fromByteArray(buffer, 0, written + 1));
+  }
+
+  @Test
+  public void toByteArray_existingByteArrayTooSmall() {
+    assertThrows(
+            IllegalArgumentException.class,
+            () -> DynamicString.constant(CONSTANT_VALUE).toDynamicStringByteArray(new byte[1]));
+  }
+
+  @Test
+  public void toByteArray_existingByteArraySameSize() {
+    DynamicString from = DynamicString.constant(CONSTANT_VALUE);
+
+    assertThat(from.toDynamicStringByteArray(new byte[100]))
+            .isEqualTo(from.toDynamicStringByteArray().length);
+  }
 }