Merge "Add new shapes to the shapes's library demo." into androidx-main
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KLibDumpParser.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KLibDumpParser.kt
index b4cb0a6..8871d69 100644
--- a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KLibDumpParser.kt
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KLibDumpParser.kt
@@ -29,6 +29,8 @@
 import org.jetbrains.kotlin.library.abi.AbiModality
 import org.jetbrains.kotlin.library.abi.AbiProperty
 import org.jetbrains.kotlin.library.abi.AbiQualifiedName
+import org.jetbrains.kotlin.library.abi.AbiSignatureVersion
+import org.jetbrains.kotlin.library.abi.AbiSignatures
 import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader
 import org.jetbrains.kotlin.library.abi.LibraryAbi
 import org.jetbrains.kotlin.library.abi.LibraryManifest
@@ -37,13 +39,13 @@
 import org.jetbrains.kotlin.library.abi.impl.AbiEnumEntryImpl
 import org.jetbrains.kotlin.library.abi.impl.AbiFunctionImpl
 import org.jetbrains.kotlin.library.abi.impl.AbiPropertyImpl
-import org.jetbrains.kotlin.library.abi.impl.AbiSignaturesImpl
 import org.jetbrains.kotlin.library.abi.impl.AbiTopLevelDeclarationsImpl
 import org.jetbrains.kotlin.library.abi.impl.AbiValueParameterImpl
 
 class MutableAbiInfo(
     val declarations: MutableList<AbiDeclaration> = mutableListOf(),
-    var uniqueName: String = ""
+    var uniqueName: String = "",
+    var signatureVersions: MutableSet<AbiSignatureVersion> = mutableSetOf()
 )
 
 @OptIn(ExperimentalLibraryAbiReader::class)
@@ -82,7 +84,7 @@
                 target to
                     LibraryAbi(
                         uniqueName = abiInfo.uniqueName,
-                        signatureVersions = emptySet(),
+                        signatureVersions = abiInfo.signatureVersions,
                         topLevelDeclarations = AbiTopLevelDeclarationsImpl(abiInfo.declarations),
                         manifest =
                             LibraryManifest(
@@ -104,20 +106,7 @@
     private fun parseDeclaration(parentQualifiedName: AbiQualifiedName?): AbiDeclaration? {
         // if the line begins with a comment, we may need to parse the current target list
         if (cursor.parseCommentMarker() != null) {
-            if (cursor.hasTargets()) {
-                // There are never targets within targets, so when we encounter a new directive we
-                // always reset our current targets
-                val targets = cursor.parseTargets()
-                targets.forEach { abiInfoByTarget.putIfAbsent(it, MutableAbiInfo()) }
-                currentTargetNames.clear()
-                currentTargetNames.addAll(targets)
-            } else if (cursor.hasUniqueName()) {
-                val uniqueName =
-                    cursor.parseUniqueName()
-                        ?: throw parseException("Failed to parse library unique name")
-                currentTargets.forEach { it.uniqueName = uniqueName }
-            }
-            cursor.nextLine()
+            parseCommentLine()
         } else if (cursor.hasClassKind()) {
             return parseClass(parentQualifiedName)
         } else if (cursor.hasFunctionKind()) {
@@ -134,6 +123,28 @@
         return null
     }
 
+    private fun parseCommentLine() {
+        if (cursor.hasTargets()) {
+            // There are never targets within targets, so when we encounter a new directive we
+            // always reset our current targets
+            val targets = cursor.parseTargets()
+            targets.forEach { abiInfoByTarget.putIfAbsent(it, MutableAbiInfo()) }
+            currentTargetNames.clear()
+            currentTargetNames.addAll(targets)
+        } else if (cursor.hasUniqueName()) {
+            val uniqueName =
+                cursor.parseUniqueName()
+                    ?: throw parseException("Failed to parse library unique name")
+            currentTargets.forEach { it.uniqueName = uniqueName }
+        } else if (cursor.hasSignatureVersion()) {
+            val signatureVersion =
+                cursor.parseSignatureVersion()
+                    ?: throw parseException("Failed to parse signature version")
+            currentTargets.forEach { it.signatureVersions.add(signatureVersion) }
+        }
+        cursor.nextLine()
+    }
+
     internal fun parseClass(parentQualifiedName: AbiQualifiedName? = null): AbiClass {
         val modality =
             cursor.parseAbiModality() ?: throw parseException("Failed to parse class modality")
@@ -157,7 +168,7 @@
             }
         return AbiClassImpl(
             qualifiedName = abiQualifiedName,
-            signatures = fakeSignatures,
+            signatures = signaturesStub,
             annotations = emptySet(), // annotations aren't part of klib dumps
             modality = modality,
             kind = kind,
@@ -210,7 +221,7 @@
         }
         return AbiPropertyImpl(
             qualifiedName = qualifiedName,
-            signatures = fakeSignatures,
+            signatures = signaturesStub,
             annotations = emptySet(), // annotations aren't part of klib dumps
             modality = modality,
             kind = kind,
@@ -230,7 +241,7 @@
         cursor.nextLine()
         return AbiEnumEntryImpl(
             qualifiedName = qualifiedName,
-            signatures = fakeSignatures,
+            signatures = signaturesStub,
             annotations = emptySet()
         )
     }
@@ -286,7 +297,7 @@
         cursor.nextLine()
         return AbiFunctionImpl(
             qualifiedName = abiQualifiedName,
-            signatures = fakeSignatures,
+            signatures = signaturesStub,
             annotations = emptySet(), // annotations aren't part of klib dumps
             modality = modality,
             isInline = isInline,
@@ -314,7 +325,7 @@
         cursor.nextLine()
         return AbiConstructorImpl(
             qualifiedName = abiQualifiedName,
-            signatures = fakeSignatures,
+            signatures = signaturesStub,
             annotations = emptySet(), // annotations aren't part of klib dumps
             isInline = false, // TODO
             contextReceiverParametersCount = 0, // TODO
@@ -356,13 +367,13 @@
         val location = "$maybeFileName${cursor.rowIndex}:${cursor.columnIndex}"
         return "$message at $location: '${cursor.currentLine}'"
     }
-
-    companion object {
-        // placeholder signatures, currently not considered during parsing / compatibility checking
-        // https://github.com/JetBrains/kotlin/blob/master/compiler/util-klib-abi/ReadMe.md
-        private val fakeSignatures = AbiSignaturesImpl(signatureV1 = null, signatureV2 = null)
-    }
 }
 
 /** Exception which uses the cursor to include the location of the failure */
 class ParseException(message: String) : RuntimeException(message)
+
+/** Signature implementation relies on internal rendering which we can't access and don't need */
+val signaturesStub =
+    object : AbiSignatures {
+        override operator fun get(signatureVersion: AbiSignatureVersion): String? = null
+    }
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensions.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensions.kt
index cba280d..63f316f 100644
--- a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensions.kt
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensions.kt
@@ -25,6 +25,7 @@
 import org.jetbrains.kotlin.library.abi.AbiModality
 import org.jetbrains.kotlin.library.abi.AbiPropertyKind
 import org.jetbrains.kotlin.library.abi.AbiQualifiedName
+import org.jetbrains.kotlin.library.abi.AbiSignatureVersion
 import org.jetbrains.kotlin.library.abi.AbiType
 import org.jetbrains.kotlin.library.abi.AbiTypeArgument
 import org.jetbrains.kotlin.library.abi.AbiTypeNullability
@@ -337,6 +338,16 @@
     return parseSymbol(uniqueNameRegex)
 }
 
+internal fun Cursor.hasSignatureVersion(): Boolean =
+    parseSymbol(signatureMarkerRegex, peek = true) != null
+
+internal fun Cursor.parseSignatureVersion(): AbiSignatureVersion? {
+    parseSymbol(signatureMarkerRegex)
+    val versionString = parseSymbol(digitRegex) ?: return null
+    val versionNumber = versionString.toInt()
+    return AbiSignatureVersion.resolveByVersionNumber(versionNumber)
+}
+
 internal fun Cursor.parseEnumEntryKind(peek: Boolean = false) =
     parseSymbol(enumEntryKindRegex, peek)
 
@@ -454,3 +465,5 @@
 private val getterOrSetterSignalRegex = Regex("^<(get|set)\\-")
 private val enumNameRegex = Regex("^[A-Z_]+")
 private val enumEntryKindRegex = Regex("^enum\\sentry")
+private val signatureMarkerRegex = Regex("-\\sSignature\\sversion:")
+private val digitRegex = Regex("\\d+")
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt
index a1cae5c..d131417 100644
--- a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/BinaryCompatibilityCheckerTest.kt
@@ -1297,7 +1297,6 @@
         // - Signature version: 2
         // - Show manifest properties: true
         // - Show declarations: true
-        
         // Library unique name: <androidx:library>
         $content
         """
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KLibDumpParserTest.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KLibDumpParserTest.kt
index 83e8ef5..0178f86 100644
--- a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KLibDumpParserTest.kt
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KLibDumpParserTest.kt
@@ -22,7 +22,9 @@
 import org.jetbrains.kotlin.library.abi.AbiCompoundName
 import org.jetbrains.kotlin.library.abi.AbiModality
 import org.jetbrains.kotlin.library.abi.AbiQualifiedName
+import org.jetbrains.kotlin.library.abi.AbiSignatureVersion
 import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader
+import org.jetbrains.kotlin.library.abi.LibraryAbi
 import org.junit.Test
 
 @OptIn(ExperimentalLibraryAbiReader::class)
@@ -230,6 +232,16 @@
     }
 
     @Test
+    fun parsesSignatureVersion() {
+        val parsed = KlibDumpParser(exampleMetadata).parse()
+        assertThat(parsed).isNotNull()
+        assertThat(parsed.keys).hasSize(1)
+        val abi: LibraryAbi = parsed.values.single()
+        assertThat(abi.signatureVersions)
+            .containsExactly(AbiSignatureVersion.resolveByVersionNumber(2))
+    }
+
+    @Test
     fun parseFullCollectionKlibDumpSucceeds() {
         val parsed = KlibDumpParser(collectionDump).parse()
         assertThat(parsed).isNotNull()
@@ -264,4 +276,18 @@
         assertThat(iosQNames).containsExactly("my.lib/myIosFun", "my.lib/commonFun")
         assertThat(linuxQNames).containsExactly("my.lib/myLinuxFun", "my.lib/commonFun")
     }
+
+    companion object {
+        private val exampleMetadata =
+            """
+            // KLib ABI Dump
+            // Targets: [linuxX64]
+            // Rendering settings:
+            // - Signature version: 2
+            // - Show manifest properties: true
+            // - Show declarations: true
+            // Library unique name: <androidx:library>
+        """
+                .trimIndent()
+    }
 }
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensionsTest.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensionsTest.kt
index 51c36da..bf0111b 100644
--- a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensionsTest.kt
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/test/java/androidx/binarycompatibilityvalidator/KlibParsingCursorExtensionsTest.kt
@@ -538,6 +538,37 @@
     }
 
     @Test
+    fun hasSignatureVersion() {
+        val input = "// - Signature version: 2"
+        val cursor = Cursor(input)
+        assertThat(cursor.hasSignatureVersion()).isTrue()
+        assertThat(cursor.currentLine).isEqualTo(input)
+    }
+
+    @Test
+    fun hasSignatureVersionFalsePositive() {
+        val input = "// - Show manifest properties: true"
+        val cursor = Cursor(input)
+        assertThat(cursor.hasSignatureVersion()).isFalse()
+    }
+
+    @Test
+    fun parseSignatureVersion() {
+        val input = "// - Signature version: 2"
+        val cursor = Cursor(input)
+        val signatureVersion = cursor.parseSignatureVersion()
+        assertThat(signatureVersion.toString()).isEqualTo("V2")
+    }
+
+    @Test
+    fun parseSignatureVersionFromTheFuture() {
+        val input = "// - Signature version: 101"
+        val cursor = Cursor(input)
+        val signatureVersion = cursor.parseSignatureVersion()
+        assertThat(signatureVersion.toString()).isEqualTo("Unsupported(versionNumber=101)")
+    }
+
+    @Test
     fun parseEnumEntryName() {
         val input = "SOME_ENUM something else"
         val cursor = Cursor(input)
diff --git a/browser/browser/src/main/res/values-uk/strings.xml b/browser/browser/src/main/res/values-uk/strings.xml
index 63a0419..f7af12a 100644
--- a/browser/browser/src/main/res/values-uk/strings.xml
+++ b/browser/browser/src/main/res/values-uk/strings.xml
@@ -16,7 +16,7 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="fallback_menu_item_open_in_browser" msgid="3413186855122069269">"Відкрити у веб-переглядачі"</string>
+    <string name="fallback_menu_item_open_in_browser" msgid="3413186855122069269">"Відкрити у вебпереглядачі"</string>
     <string name="fallback_menu_item_copy_link" msgid="4566929209979330987">"Копіювати посилання"</string>
     <string name="fallback_menu_item_share_link" msgid="7145444925855055364">"Надіслати посилання"</string>
     <string name="copy_toast_msg" msgid="3260749812566568062">"Посилання скопійовано в буфер обміну"</string>
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 1465e39..10cb49a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -1381,7 +1381,6 @@
 
     companion object {
         const val CREATE_LIBRARY_BUILD_INFO_FILES_TASK = "createLibraryBuildInfoFiles"
-        const val GENERATE_TEST_CONFIGURATION_TASK = "GenerateTestConfiguration"
         const val FINALIZE_TEST_CONFIGS_WITH_APKS_TASK = "finalizeTestConfigsWithApks"
         const val ZIP_TEST_CONFIGS_WITH_APKS_TASK = "zipTestConfigsWithApks"
 
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt b/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt
index 549bacc..cc9782a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/BinaryCompatibilityValidation.kt
@@ -159,7 +159,7 @@
                     KotlinKlibExtractSupportedTargetsAbiTask::class.java
                 ) {
                     it.strictValidation = true
-                    it.supportedTargets = project.provider { supportedTargets() }
+                    it.supportedTargets = project.provider { supportedNativeTargetNames() }
                     it.inputAbiFile = requiredCompatFile
                     it.outputAbiFile = klibExtractDir.resolve(requiredCompatFile.name)
                     (it as DefaultTask).group = ABI_GROUP_NAME
@@ -195,6 +195,7 @@
             it.version.set(projectVersion)
             it.shouldWriteVersionedApiFile.set(project.shouldWriteVersionedApiFile())
             it.group = ABI_GROUP_NAME
+            it.unsupportedNativeTargetNames.set(unsupportedNativeTargetNames())
         }
 
     /**
@@ -205,7 +206,7 @@
     private fun Project.extractKlibAbiTask(klibApiDir: File, extractDir: File) =
         project.tasks.register(EXTRACT_NAME, KotlinKlibExtractSupportedTargetsAbiTask::class.java) {
             it.strictValidation = true
-            it.supportedTargets = project.provider { supportedTargets() }
+            it.supportedTargets = project.provider { supportedNativeTargetNames() }
             it.inputAbiFile = klibApiDir.resolve(CURRENT_API_FILE_NAME)
             it.outputAbiFile = extractDir.resolve(CURRENT_API_FILE_NAME)
             (it as DefaultTask).group = ABI_GROUP_NAME
@@ -247,19 +248,21 @@
         }
     }
 
-    private fun supportedTargets(): Set<String> {
+    private fun supportedNativeTargetNames(): Set<String> {
         val hostManager = HostManager()
-        return kotlinMultiplatformExtension.targets
-            .matching { it.platformType == KotlinPlatformType.native }
-            .asSequence()
-            .filterIsInstance<KotlinNativeTarget>()
+        return kotlinMultiplatformExtension
+            .nativeTargets()
             .filter { hostManager.isEnabled(it.konanTarget) }
-            .map {
-                KlibTarget(it.targetName, konanTargetNameMapping[it.konanTarget.name]!!).toString()
-            }
+            .map { it.klibTargetName() }
             .toSet()
     }
 
+    private fun allNativeTargetNames(): Set<String> =
+        kotlinMultiplatformExtension.nativeTargets().map { it.klibTargetName() }.toSet()
+
+    private fun unsupportedNativeTargetNames(): Set<String> =
+        allNativeTargetNames() - supportedNativeTargetNames()
+
     private fun Project.configureKlibCompilation(
         compilation: KotlinCompilation<KotlinCommonOptions>,
         targetName: String,
@@ -289,4 +292,9 @@
     )
 
 private fun KotlinMultiplatformExtension.nativeTargets() =
-    targets.matching { it.platformType == KotlinPlatformType.native }
+    targets.withType(KotlinNativeTarget::class.java).matching {
+        it.platformType == KotlinPlatformType.native
+    }
+
+private fun KotlinNativeTarget.klibTargetName(): String =
+    KlibTarget(targetName, konanTargetNameMapping[konanTarget.name]!!).toString()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/UpdateAbiTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/UpdateAbiTask.kt
index 753a5b3..c28899f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/UpdateAbiTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/binarycompatibilityvalidator/UpdateAbiTask.kt
@@ -19,7 +19,9 @@
 import java.io.File
 import javax.inject.Inject
 import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
 import org.gradle.api.file.FileSystemOperations
+import org.gradle.api.provider.ListProperty
 import org.gradle.api.provider.Property
 import org.gradle.api.provider.Provider
 import org.gradle.api.tasks.CacheableTask
@@ -39,6 +41,8 @@
 
     @get:Input abstract val shouldWriteVersionedApiFile: Property<Boolean>
 
+    @get:Input abstract val unsupportedNativeTargetNames: ListProperty<String>
+
     /** Text file from which API signatures will be read. */
     @get:PathSensitive(PathSensitivity.RELATIVE)
     @get:InputFile
@@ -49,6 +53,14 @@
 
     @TaskAction
     fun execute() {
+        unsupportedNativeTargetNames.get().let { targets ->
+            if (targets.isNotEmpty()) {
+                throw GradleException(
+                    "Cannot update API files because the current host doesn't support the " +
+                        "following targets: ${targets.joinToString(", ")}"
+                )
+            }
+        }
         fileSystemOperations.copy {
             it.from(inputApiLocation)
             it.into(outputDir)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyTestApksTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyTestApksTask.kt
new file mode 100644
index 0000000..ffa92365
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/CopyTestApksTask.kt
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.testConfiguration
+
+import com.android.build.api.variant.BuiltArtifactsLoader
+import java.io.File
+import javax.inject.Inject
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.model.ObjectFactory
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.SkipWhenEmpty
+import org.gradle.api.tasks.TaskAction
+import org.gradle.work.DisableCachingByDefault
+
+/** Copy APKs needed for building androidTest.zip */
+@DisableCachingByDefault(because = "Only filesystem operations")
+abstract class CopyTestApksTask @Inject constructor(private val objects: ObjectFactory) :
+    DefaultTask() {
+
+    /** File existence check to determine whether to run this task. */
+    @get:InputFiles
+    @get:SkipWhenEmpty
+    @get:PathSensitive(PathSensitivity.NONE)
+    abstract val androidTestSourceCode: ConfigurableFileCollection
+
+    @get:InputFiles
+    @get:PathSensitive(PathSensitivity.NONE)
+    abstract val testFolder: DirectoryProperty
+
+    @get:InputFiles
+    @get:Optional
+    @get:PathSensitive(PathSensitivity.NONE)
+    abstract val appFolder: DirectoryProperty
+
+    @get:InputFiles
+    @get:PathSensitive(PathSensitivity.NONE)
+    abstract val appFileCollection: ConfigurableFileCollection
+
+    /**
+     * Extracted APKs for PrivacySandbox SDKs dependencies. Produced by AGP.
+     *
+     * Should be set only for applications with PrivacySandbox SDKs dependencies.
+     */
+    @get:InputFiles
+    @get:Optional
+    @get:PathSensitive(PathSensitivity.RELATIVE) // We use parent folder for file name generation
+    abstract val privacySandboxSdkApks: ConfigurableFileCollection
+
+    /**
+     * Extracted split with manifest containing <uses-sdk-library> tag. Produced by AGP.
+     *
+     * Should be set only for applications with PrivacySandbox SDKs dependencies.
+     */
+    @get:InputFiles
+    @get:Optional
+    @get:PathSensitive(PathSensitivity.NAME_ONLY)
+    abstract val privacySandboxUsesSdkSplit: ConfigurableFileCollection
+
+    /**
+     * Extracted compat splits for PrivacySandbox SDKs dependencies. Produced by AGP.
+     *
+     * Should be set only for applications with PrivacySandbox SDKs dependencies.
+     */
+    @get:InputFiles
+    @get:Optional
+    @get:PathSensitive(PathSensitivity.NAME_ONLY)
+    abstract val privacySandboxSdkCompatSplits: ConfigurableFileCollection
+
+    /**
+     * Filename prefix for all PrivacySandbox related output files. Required for producing unique
+     * filenames over all projects.
+     *
+     * Should be set only for applications with PrivacySandbox SDKs dependencies.
+     */
+    @get:Input @get:Optional abstract val filenamePrefixForPrivacySandboxFiles: Property<String>
+
+    @get:Internal abstract val appLoader: Property<BuiltArtifactsLoader>
+
+    @get:Internal abstract val testLoader: Property<BuiltArtifactsLoader>
+
+    @get:OutputFile abstract val outputApplicationId: RegularFileProperty
+
+    @get:OutputFile abstract val outputTestApk: RegularFileProperty
+
+    @get:[OutputFile Optional]
+    abstract val outputAppApk: RegularFileProperty
+
+    /**
+     * Output directory for PrivacySandbox SDKs APKs.
+     *
+     * Should be set only for applications with PrivacySandbox SDKs dependencies.
+     */
+    @get:[OutputDirectory Optional]
+    abstract val outputPrivacySandboxSdkApks: DirectoryProperty
+
+    /**
+     * Output directory for App splits required for devices with PrivacySandbox support.
+     *
+     * Should be set only for applications with PrivacySandbox SDKs dependencies.
+     */
+    @get:[OutputDirectory Optional]
+    abstract val outputPrivacySandboxAppSplits: DirectoryProperty
+
+    /**
+     * Output directory for App splits required for devices without PrivacySandbox support.
+     *
+     * Should be set only for applications with PrivacySandbox SDKs dependencies.
+     */
+    @get:[OutputDirectory Optional]
+    abstract val outputPrivacySandboxCompatAppSplits: DirectoryProperty
+
+    @TaskAction
+    fun createApks() {
+        if (appLoader.isPresent) {
+            // Decides where to load the app apk from, depending on whether appFolder or
+            // appFileCollection has been set.
+            val appDir =
+                if (appFolder.isPresent && appFileCollection.files.isEmpty()) {
+                    appFolder.get()
+                } else if (!appFolder.isPresent && appFileCollection.files.size == 1) {
+                    objects
+                        .directoryProperty()
+                        .also { it.set(appFileCollection.files.first()) }
+                        .get()
+                } else {
+                    throw IllegalStateException(
+                        """
+                    App apk not specified or both appFileCollection and appFolder specified.
+                """
+                            .trimIndent()
+                    )
+                }
+
+            val appApk =
+                appLoader.get().load(appDir)
+                    ?: throw RuntimeException("Cannot load required APK for task: $name")
+            // We don't need to check hasBenchmarkPlugin because benchmarks shouldn't have test apps
+            val appApkBuiltArtifact = appApk.elements.single()
+            val destinationApk = outputAppApk.get().asFile
+            File(appApkBuiltArtifact.outputFile).copyTo(destinationApk, overwrite = true)
+
+            createPrivacySandboxFiles()
+        }
+
+        val testApk =
+            testLoader.get().load(testFolder.get())
+                ?: throw RuntimeException("Cannot load required APK for task: $name")
+        val testApkBuiltArtifact = testApk.elements.single()
+        val destinationApk = outputTestApk.get().asFile
+        File(testApkBuiltArtifact.outputFile).copyTo(destinationApk, overwrite = true)
+
+        val outputApplicationIdFile = outputApplicationId.get().asFile
+        outputApplicationIdFile.bufferedWriter().use { out -> out.write(testApk.applicationId) }
+    }
+
+    /**
+     * Creates APKs required for running App with PrivacySandbox SDKs. Do nothing if project doesn't
+     * have dependencies on PrivacySandbox SDKs.
+     */
+    private fun createPrivacySandboxFiles() {
+        if (privacySandboxSdkApks.isEmpty) {
+            return
+        }
+
+        val prefix = filenamePrefixForPrivacySandboxFiles.get()
+
+        privacySandboxSdkApks.asFileTree.map { sdkApk ->
+            // TODO (b/309610890): Remove after supporting unique filenames on bundletool side.
+            val sdkProjectName = sdkApk.parentFile?.name
+            val outputFileName = "$prefix-$sdkProjectName-${sdkApk.name}"
+            val outputFile = outputPrivacySandboxSdkApks.get().file(outputFileName)
+            sdkApk.copyTo(outputFile.asFile, overwrite = true)
+        }
+
+        val usesSdkSplitArtifact =
+            appLoader.get().load(privacySandboxUsesSdkSplit)?.elements?.single()
+        if (usesSdkSplitArtifact != null) {
+            val splitApk = File(usesSdkSplitArtifact.outputFile)
+            val outputFileName = "$prefix-${splitApk.name}"
+            val outputFile = outputPrivacySandboxAppSplits.get().file(outputFileName)
+            splitApk.copyTo(outputFile.asFile, overwrite = true)
+        }
+
+        appLoader.get().load(privacySandboxSdkCompatSplits)?.elements?.forEach { splitArtifact ->
+            val splitApk = File(splitArtifact.outputFile)
+            val outputFileName = "$prefix-${splitApk.name}"
+            val outputFile = outputPrivacySandboxCompatAppSplits.get().file(outputFileName)
+            splitApk.copyTo(outputFile.asFile, overwrite = true)
+        }
+    }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
index b26463e..5b57b81 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
@@ -16,23 +16,18 @@
 
 package androidx.build.testConfiguration
 
-import com.android.build.api.variant.BuiltArtifactsLoader
 import java.io.File
-import javax.inject.Inject
 import org.gradle.api.DefaultTask
 import org.gradle.api.GradleException
 import org.gradle.api.file.ConfigurableFileCollection
-import org.gradle.api.file.DirectoryProperty
 import org.gradle.api.file.RegularFileProperty
-import org.gradle.api.model.ObjectFactory
 import org.gradle.api.provider.ListProperty
 import org.gradle.api.provider.MapProperty
 import org.gradle.api.provider.Property
 import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
 import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.Internal
 import org.gradle.api.tasks.Optional
-import org.gradle.api.tasks.OutputDirectory
 import org.gradle.api.tasks.OutputFile
 import org.gradle.api.tasks.PathSensitive
 import org.gradle.api.tasks.PathSensitivity
@@ -43,47 +38,49 @@
 /**
  * Writes a configuration file in <a
  * href=https://source.android.com/devices/tech/test_infra/tradefed/testing/through-suite/android-test-structure>AndroidTest.xml</a>
- * format that gets zipped alongside the APKs to be tested. This config gets ingested by Tradefed.
+ * format that gets zipped alongside the APKs to be tested.
+ *
+ * Generates XML for Tradefed test infrastructure and JSON for FTL test infrastructure.
  */
 @DisableCachingByDefault(because = "Doesn't benefit from caching")
-abstract class GenerateTestConfigurationTask
-@Inject
-constructor(private val objects: ObjectFactory) : DefaultTask() {
+abstract class GenerateTestConfigurationTask : DefaultTask() {
 
-    @get:InputFiles
+    @get:InputFile
     @get:Optional
-    @get:PathSensitive(PathSensitivity.RELATIVE)
-    abstract val appFolder: DirectoryProperty
-
-    @get:InputFiles
-    @get:PathSensitive(PathSensitivity.RELATIVE)
-    abstract val appFileCollection: ConfigurableFileCollection
+    @get:PathSensitive(PathSensitivity.NAME_ONLY)
+    abstract val appApk: RegularFileProperty
 
     /** File existence check to determine whether to run this task. */
     @get:InputFiles
     @get:SkipWhenEmpty
-    @get:PathSensitive(PathSensitivity.RELATIVE)
+    @get:PathSensitive(PathSensitivity.NONE)
     abstract val androidTestSourceCodeCollection: ConfigurableFileCollection
 
-    @get:Internal abstract val appLoader: Property<BuiltArtifactsLoader>
-
-    /** Extracted APKs for PrivacySandbox SDKs dependencies. Produced by AGP. */
+    /**
+     * Extracted APKs for PrivacySandbox SDKs dependencies. Produced by AGP.
+     *
+     * Should be set only for applications with PrivacySandbox SDKs dependencies.
+     */
     @get:InputFiles
     @get:Optional
-    @get:PathSensitive(PathSensitivity.RELATIVE)
+    @get:PathSensitive(PathSensitivity.NAME_ONLY)
     abstract val privacySandboxSdkApks: ConfigurableFileCollection
 
-    /** Extracted split with manifest containing <uses-sdk-library> tag. Produced by AGP. */
+    /**
+     * Extracted splits required for running app with PrivacySandbox SDKs. Produced by AGP.
+     *
+     * Should be set only for applications with PrivacySandbox SDKs dependencies.
+     */
     @get:InputFiles
     @get:Optional
-    @get:PathSensitive(PathSensitivity.RELATIVE)
-    abstract val privacySandboxUsesSdkSplit: ConfigurableFileCollection
+    @get:PathSensitive(PathSensitivity.NAME_ONLY)
+    abstract val privacySandboxAppSplits: ConfigurableFileCollection
 
-    @get:InputFiles
-    @get:PathSensitive(PathSensitivity.RELATIVE)
-    abstract val testFolder: DirectoryProperty
+    @get:InputFile
+    @get:PathSensitive(PathSensitivity.NAME_ONLY)
+    abstract val testApk: RegularFileProperty
 
-    @get:Internal abstract val testLoader: Property<BuiltArtifactsLoader>
+    @get:Input abstract val applicationId: Property<String>
 
     @get:Input abstract val minSdk: Property<Int>
 
@@ -103,22 +100,13 @@
 
     @get:OutputFile abstract val outputXml: RegularFileProperty
 
-    @get:OutputFile abstract val outputJson: RegularFileProperty
-
-    @get:OutputFile abstract val outputTestApk: RegularFileProperty
-
-    @get:[OutputFile Optional]
-    abstract val outputAppApk: RegularFileProperty
-
     /**
-     * Filename prefix for all PrivacySandbox related output files. Required for producing unique
-     * filenames over all projects,
+     * Optional as privacy sandbox not yet supported in JSON configs.
+     *
+     * TODO (b/347315428): Support privacy sandbox on FTL.
      */
-    @get:Input @get:Optional abstract val outputPrivacySandboxFilenamesPrefix: Property<String>
-
-    /** Output directory for PrivacySandbox files (SDKs APKs, splits, etc). */
-    @get:[OutputDirectory Optional]
-    abstract val outputPrivacySandboxFiles: DirectoryProperty
+    @get:[OutputFile Optional]
+    abstract val outputJson: RegularFileProperty
 
     @TaskAction
     fun generateAndroidTestZip() {
@@ -130,39 +118,18 @@
          */
         val configBuilder = ConfigBuilder()
         configBuilder.configName = outputXml.asFile.get().name
-        if (appLoader.isPresent) {
-
-            // Decides where to load the app apk from, depending on whether appFolder or
-            // appFileCollection has been set.
-            val appDir =
-                if (appFolder.isPresent && appFileCollection.files.isEmpty()) {
-                    appFolder.get()
-                } else if (!appFolder.isPresent && appFileCollection.files.size == 1) {
-                    objects
-                        .directoryProperty()
-                        .also { it.set(appFileCollection.files.first()) }
-                        .get()
-                } else {
-                    throw IllegalStateException(
-                        """
-                    App apk not specified or both appFileCollection and appFolder specified.
-                """
-                            .trimIndent()
-                    )
-                }
-
-            val appApk =
-                appLoader.get().load(appDir)
-                    ?: throw RuntimeException("Cannot load required APK for task: $name")
-            // We don't need to check hasBenchmarkPlugin because benchmarks shouldn't have test apps
-            val appApkBuiltArtifact = appApk.elements.single()
-            val destinationApk = outputAppApk.get().asFile
-            File(appApkBuiltArtifact.outputFile).copyTo(destinationApk, overwrite = true)
-            configBuilder
-                .appApkName(destinationApk.name)
-                .appApkSha256(sha256(File(appApkBuiltArtifact.outputFile)))
-            configurePrivacySandbox(configBuilder)
+        if (appApk.isPresent) {
+            val appApkFile = appApk.get().asFile
+            configBuilder.appApkName(appApkFile.name).appApkSha256(sha256(appApkFile))
         }
+
+        val privacySandboxSdkApksFileNames =
+            privacySandboxSdkApks.asFileTree.map { f -> f.name }.sorted()
+        configBuilder.initialSetupApks(privacySandboxSdkApksFileNames)
+        val privacySandboxSplitsFileNames =
+            privacySandboxAppSplits.asFileTree.map { f -> f.name }.sorted()
+        configBuilder.appSplits(privacySandboxSplitsFileNames)
+
         configBuilder.additionalApkKeys(additionalApkKeys.get())
         val isPresubmit = presubmit.get()
         configBuilder.isPostsubmit(!isPresubmit)
@@ -193,79 +160,29 @@
             configBuilder.tag("androidx_unit_tests")
         }
         additionalTags.get().forEach { configBuilder.tag(it) }
-        val testApk =
-            testLoader.get().load(testFolder.get())
-                ?: throw RuntimeException("Cannot load required APK for task: $name")
-        val testApkBuiltArtifact = testApk.elements.single()
-        val destinationApk = outputTestApk.get().asFile
-        File(testApkBuiltArtifact.outputFile).copyTo(destinationApk, overwrite = true)
         instrumentationArgs.get().forEach { (key, value) ->
             configBuilder.instrumentationArgsMap[key] = value
         }
+        val testApkFile = testApk.get().asFile
         configBuilder
-            .testApkName(destinationApk.name)
-            .applicationId(testApk.applicationId)
+            .testApkName(testApkFile.name)
+            .applicationId(applicationId.get())
             .minSdk(minSdk.get().toString())
             .testRunner(testRunner.get())
-            .testApkSha256(sha256(File(testApkBuiltArtifact.outputFile)))
+            .testApkSha256(sha256(testApkFile))
         createOrFail(outputXml).writeText(configBuilder.buildXml())
-        if (!outputJson.asFile.get().name.startsWith("_")) {
-            // Prefixing json file names with _ allows us to collocate these files
-            // inside of the androidTest.zip to make fetching them less expensive.
-            throw GradleException(
-                "json output file names are expected to use _ prefix to, " +
-                    "currently set to ${outputJson.asFile.get().name}"
-            )
-        }
-        if (privacySandboxSdkApks.isEmpty) {
-            // Privacy sandbox not yet supported in JSON configs
+        if (outputJson.isPresent) {
+            if (!outputJson.asFile.get().name.startsWith("_")) {
+                // Prefixing json file names with _ allows us to collocate these files
+                // inside of the androidTest.zip to make fetching them less expensive.
+                throw GradleException(
+                    "json output file names are expected to use _ prefix to, " +
+                        "currently set to ${outputJson.asFile.get().name}"
+                )
+            }
             createOrFail(outputJson).writeText(configBuilder.buildJson())
         }
     }
-
-    /**
-     * Configure installation of PrivacySandbox SDKs before main and test APKs. Do nothing if
-     * project doesn't have dependencies on PrivacySandbox SDKs.
-     */
-    private fun configurePrivacySandbox(configBuilder: ConfigBuilder) {
-        if (privacySandboxSdkApks.isEmpty) {
-            return
-        }
-
-        val prefix = outputPrivacySandboxFilenamesPrefix.get()
-        val sdkApkFileNames =
-            privacySandboxSdkApks.asFileTree.map { sdkApk ->
-                // TODO (b/309610890): Remove after supporting unique filenames on bundletool side.
-                val sdkProjectName = sdkApk.parentFile?.name
-                val outputFileName = "$prefix-$sdkProjectName-${sdkApk.name}"
-                val outputFile = outputPrivacySandboxFiles.get().file(outputFileName)
-                sdkApk.copyTo(outputFile.asFile, overwrite = true)
-                outputFileName
-            }
-        configBuilder.initialSetupApks(sdkApkFileNames)
-
-        val usesSdkSplitArtifact =
-            appLoader.get().load(privacySandboxUsesSdkSplit)?.elements?.single()
-        if (usesSdkSplitArtifact != null) {
-            val splitApk = File(usesSdkSplitArtifact.outputFile)
-            val outputFileName = "$prefix-${splitApk.name}"
-            val outputFile = outputPrivacySandboxFiles.get().file(outputFileName)
-            splitApk.copyTo(outputFile.asFile, overwrite = true)
-            configBuilder.appSplits(listOf(outputFileName))
-        }
-
-        if (minSdk.get() < PRIVACY_SANDBOX_MIN_API_LEVEL) {
-            /*
-            Privacy Sandbox SDKs could be installed starting from PRIVACY_SANDBOX_MIN_API_LEVEL.
-            Separate compat config will be generated for lower api levels.
-            */
-            configBuilder.minSdk(PRIVACY_SANDBOX_MIN_API_LEVEL.toString())
-        }
-    }
-
-    companion object {
-        private const val PRIVACY_SANDBOX_MIN_API_LEVEL = 34
-    }
 }
 
 internal fun createOrFail(fileProperty: RegularFileProperty): File {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index 95477657..5787c86 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -17,7 +17,6 @@
 package androidx.build.testConfiguration
 
 import androidx.build.AndroidXExtension
-import androidx.build.AndroidXImplPlugin
 import androidx.build.AndroidXImplPlugin.Companion.FINALIZE_TEST_CONFIGS_WITH_APKS_TASK
 import androidx.build.asFilenamePrefix
 import androidx.build.dependencyTracker.AffectedModuleDetector
@@ -44,9 +43,11 @@
 import com.android.build.api.variant.TestAndroidComponentsExtension
 import com.android.build.api.variant.TestVariant
 import com.android.build.api.variant.Variant
+import kotlin.math.max
 import org.gradle.api.Project
 import org.gradle.api.artifacts.type.ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE
 import org.gradle.api.attributes.Usage
+import org.gradle.api.file.Directory
 import org.gradle.api.file.FileCollection
 import org.gradle.api.file.RegularFile
 import org.gradle.api.provider.Provider
@@ -69,62 +70,112 @@
     instrumentationRunnerArgs: Provider<Map<String, String>>,
     variant: Variant?
 ) {
-    val xmlName = "${path.asFilenamePrefix()}$variantName.xml"
-    val jsonName = "_${path.asFilenamePrefix()}$variantName.json"
-    rootProject.tasks.named<ModuleInfoGenerator>("createModuleInfo").configure {
-        it.testModules.add(
-            TestModule(
-                name = xmlName,
-                path = listOf(projectDir.toRelativeString(getSupportRootFolder()))
-            )
+    val copyTestApksTask = registerCopyTestApksTask(variantName, artifacts, variant)
+
+    if (isPrivacySandboxEnabled()) {
+        /*
+        Privacy Sandbox SDKs could be installed starting from PRIVACY_SANDBOX_MIN_API_LEVEL.
+        Separate compat config generated for lower api levels.
+        */
+        registerGenerateTestConfigurationTask(
+            "${GENERATE_PRIVACY_SANDBOX_MAIN_TEST_CONFIGURATION_TASK}$variantName",
+            xmlName = "${path.asFilenamePrefix()}$variantName.xml",
+            jsonName = null, // Privacy sandbox not yet supported in JSON configs
+            copyTestApksTask.flatMap { it.outputApplicationId },
+            copyTestApksTask.flatMap { it.outputAppApk },
+            copyTestApksTask.flatMap { it.outputTestApk },
+            copyTestApksTask.flatMap { it.outputPrivacySandboxSdkApks },
+            copyTestApksTask.flatMap { it.outputPrivacySandboxAppSplits },
+            minSdk = max(minSdk, PRIVACY_SANDBOX_MIN_API_LEVEL),
+            testRunner,
+            instrumentationRunnerArgs,
+            variant
+        )
+
+        registerGenerateTestConfigurationTask(
+            "${GENERATE_PRIVACY_SANDBOX_COMPAT_TEST_CONFIGURATION_TASK}${variantName}",
+            xmlName = "${path.asFilenamePrefix()}${variantName}Compat.xml",
+            jsonName = null, // Privacy sandbox not yet supported in JSON configs
+            copyTestApksTask.flatMap { it.outputApplicationId },
+            copyTestApksTask.flatMap { it.outputAppApk },
+            copyTestApksTask.flatMap { it.outputTestApk },
+            privacySandboxApks = null,
+            copyTestApksTask.flatMap { it.outputPrivacySandboxCompatAppSplits },
+            minSdk,
+            testRunner,
+            instrumentationRunnerArgs,
+            variant
+        )
+    } else {
+        registerGenerateTestConfigurationTask(
+            "${GENERATE_TEST_CONFIGURATION_TASK}$variantName",
+            xmlName = "${path.asFilenamePrefix()}$variantName.xml",
+            jsonName = "_${path.asFilenamePrefix()}$variantName.json",
+            copyTestApksTask.flatMap { it.outputApplicationId },
+            copyTestApksTask.flatMap { it.outputAppApk },
+            copyTestApksTask.flatMap { it.outputTestApk },
+            privacySandboxApks = null,
+            privacySandboxSplits = null,
+            minSdk,
+            testRunner,
+            instrumentationRunnerArgs,
+            variant
         )
     }
-    val generateTestConfigurationTask =
-        tasks.register(
-            "${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}$variantName",
-            GenerateTestConfigurationTask::class.java
-        ) { task ->
-            val androidXExtension = extensions.getByType<AndroidXExtension>()
-            if (isPrivacySandboxEnabled()) {
-                // TODO (b/309610890): Replace for dependency on AGP artifact.
-                val extractedPrivacySandboxSdkApksDir =
-                    layout.buildDirectory.dir(
-                        "intermediates/extracted_apks_from_privacy_sandbox_sdks"
-                    )
-                task.privacySandboxSdkApks.from(
-                    files(extractedPrivacySandboxSdkApksDir) {
-                        it.builtBy("buildPrivacySandboxSdkApksForDebug")
-                    }
-                )
-                // TODO (b/309610890): Replace for dependency on AGP artifact.
-                val usesSdkSplitDir =
-                    layout.buildDirectory.dir(
-                        "intermediates/uses_sdk_library_split_for_local_deployment"
-                    )
-                task.privacySandboxUsesSdkSplit.from(
-                    files(usesSdkSplitDir) {
-                        it.builtBy("generateDebugAdditionalSplitForPrivacySandboxDeployment")
-                    }
-                )
-                task.outputPrivacySandboxFilenamesPrefix.set(
-                    "${path.asFilenamePrefix()}-$variantName"
-                )
-                task.outputPrivacySandboxFiles.set(
-                    getPrivacySandboxFilesDirectory().map {
-                        it.dir("${path.asFilenamePrefix()}-$variantName")
-                    }
-                )
-            }
+}
 
-            task.testFolder.set(artifacts.get(SingleArtifact.APK))
-            task.testLoader.set(artifacts.getBuiltArtifactsLoader())
-            task.outputTestApk.set(
-                getFileInTestConfigDirectory("${path.asFilenamePrefix()}-$variantName.apk")
-            )
+private fun Project.registerCopyTestApksTask(
+    variantName: String,
+    artifacts: Artifacts,
+    variant: Variant?
+): TaskProvider<CopyTestApksTask> {
+    return tasks.register("${COPY_TEST_APKS_TASK}$variantName", CopyTestApksTask::class.java) { task
+        ->
+        task.testFolder.set(artifacts.get(SingleArtifact.APK))
+        task.testLoader.set(artifacts.getBuiltArtifactsLoader())
+
+        task.outputApplicationId.set(layout.buildDirectory.file("$variantName-appId.txt"))
+        task.outputTestApk.set(
+            getFileInTestConfigDirectory("${path.asFilenamePrefix()}-$variantName.apk")
+        )
+
+        // Skip task if getTestSourceSetsForAndroid is empty, even if
+        //  androidXExtension.deviceTests.enabled is set to true
+        task.androidTestSourceCode.from(getTestSourceSetsForAndroid(variant))
+        val androidXExtension = extensions.getByType<AndroidXExtension>()
+        task.enabled = androidXExtension.deviceTests.enabled
+        AffectedModuleDetector.configureTaskGuard(task)
+    }
+}
+
+private fun Project.registerGenerateTestConfigurationTask(
+    taskName: String,
+    xmlName: String,
+    jsonName: String?,
+    applicationIdFile: Provider<RegularFile>,
+    appApk: Provider<RegularFile>,
+    testApk: Provider<RegularFile>,
+    privacySandboxApks: Provider<Directory>?,
+    privacySandboxSplits: Provider<Directory>?,
+    minSdk: Int,
+    testRunner: Provider<String>,
+    instrumentationRunnerArgs: Provider<Map<String, String>>,
+    variant: Variant?
+) {
+    val generateTestConfigurationTask =
+        tasks.register(taskName, GenerateTestConfigurationTask::class.java) { task ->
+            task.applicationId.set(project.providers.fileContents(applicationIdFile).asText)
+            task.appApk.set(appApk)
+            task.testApk.set(testApk)
+
+            privacySandboxApks?.let { task.privacySandboxSdkApks.from(it) }
+            privacySandboxSplits?.let { task.privacySandboxAppSplits.from(it) }
+
+            val androidXExtension = extensions.getByType<AndroidXExtension>()
             task.additionalApkKeys.set(androidXExtension.additionalDeviceTestApkKeys)
             task.additionalTags.set(androidXExtension.additionalDeviceTestTags)
             task.outputXml.set(getFileInTestConfigDirectory(xmlName))
-            task.outputJson.set(getFileInTestConfigDirectory(jsonName))
+            jsonName?.let { task.outputJson.set(getFileInTestConfigDirectory(it)) }
             task.presubmit.set(isPresubmitBuild())
             task.instrumentationArgs.putAll(instrumentationRunnerArgs)
             task.minSdk.set(minSdk)
@@ -140,6 +191,14 @@
     rootProject.tasks
         .findByName(FINALIZE_TEST_CONFIGS_WITH_APKS_TASK)!!
         .dependsOn(generateTestConfigurationTask)
+    rootProject.tasks.named<ModuleInfoGenerator>("createModuleInfo").configure {
+        it.testModules.add(
+            TestModule(
+                name = xmlName,
+                path = listOf(projectDir.toRelativeString(getSupportRootFolder()))
+            )
+        )
+    }
 }
 
 /**
@@ -162,18 +221,65 @@
         return getFileInTestConfigDirectory(filename)
     }
 
+    fun addPrivacySandboxApksFor(variant: Variant, task: CopyTestApksTask) {
+        // TODO (b/309610890): Replace for dependency on AGP artifact.
+        val extractedPrivacySandboxSdkApksDir =
+            layout.buildDirectory.dir("intermediates/extracted_apks_from_privacy_sandbox_sdks")
+        task.privacySandboxSdkApks.from(
+            files(extractedPrivacySandboxSdkApksDir) {
+                it.builtBy("buildPrivacySandboxSdkApksForDebug")
+            }
+        )
+        // TODO (b/309610890): Replace for dependency on AGP artifact.
+        val usesSdkSplitDir =
+            layout.buildDirectory.dir("intermediates/uses_sdk_library_split_for_local_deployment")
+        task.privacySandboxUsesSdkSplit.from(
+            files(usesSdkSplitDir) {
+                it.builtBy("generateDebugAdditionalSplitForPrivacySandboxDeployment")
+            }
+        )
+        // TODO (b/309610890): Replace for dependency on AGP artifact.
+        val extractedPrivacySandboxCompatSplitsDir =
+            layout.buildDirectory.dir("intermediates/extracted_sdk_apks")
+        task.privacySandboxSdkCompatSplits.from(
+            files(extractedPrivacySandboxCompatSplitsDir) {
+                it.builtBy("extractApksFromSdkSplitsForDebug")
+            }
+        )
+        task.filenamePrefixForPrivacySandboxFiles.set("${path.asFilenamePrefix()}-${variant.name}")
+        task.outputPrivacySandboxSdkApks.set(
+            getPrivacySandboxFilesDirectory().map {
+                it.dir("${path.asFilenamePrefix()}-${variant.name}-sdks")
+            }
+        )
+        task.outputPrivacySandboxAppSplits.set(
+            getPrivacySandboxFilesDirectory().map {
+                it.dir("${path.asFilenamePrefix()}-${variant.name}-app-splits")
+            }
+        )
+        task.outputPrivacySandboxCompatAppSplits.set(
+            getPrivacySandboxFilesDirectory().map {
+                it.dir("${path.asFilenamePrefix()}-${variant.name}-compat-app-splits")
+            }
+        )
+    }
+
     // For application modules, the instrumentation apk is generated in the module itself
     extensions.findByType(ApplicationAndroidComponentsExtension::class.java)?.apply {
         onVariants(selector().withBuildType("debug")) { variant ->
             tasks.named(
-                "${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}${variant.name}AndroidTest",
-                GenerateTestConfigurationTask::class.java
+                "${COPY_TEST_APKS_TASK}${variant.name}AndroidTest",
+                CopyTestApksTask::class.java
             ) { task ->
                 task.appFolder.set(variant.artifacts.get(SingleArtifact.APK))
                 task.appLoader.set(variant.artifacts.getBuiltArtifactsLoader())
 
                 // The target project is the same being evaluated
                 task.outputAppApk.set(outputAppApkFile(variant, path, null))
+
+                if (isPrivacySandboxEnabled()) {
+                    addPrivacySandboxApksFor(variant, task)
+                }
             }
         }
     }
@@ -184,10 +290,8 @@
     // from the application one.
     extensions.findByType(TestAndroidComponentsExtension::class.java)?.apply {
         onVariants(selector().all()) { variant ->
-            tasks.named(
-                "${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}${variant.name}",
-                GenerateTestConfigurationTask::class.java
-            ) { task ->
+            tasks.named("${COPY_TEST_APKS_TASK}${variant.name}", CopyTestApksTask::class.java) {
+                task ->
                 task.appLoader.set(variant.artifacts.getBuiltArtifactsLoader())
 
                 // The target app path is defined in the targetProjectPath field in the android
@@ -245,8 +349,8 @@
                 }
 
             tasks.named(
-                "${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}${variant.name}AndroidTest",
-                GenerateTestConfigurationTask::class.java
+                "${COPY_TEST_APKS_TASK}${variant.name}AndroidTest",
+                CopyTestApksTask::class.java
             ) { task ->
                 task.appLoader.set(variant.artifacts.getBuiltArtifactsLoader())
 
@@ -273,11 +377,11 @@
         !parentProject.tasks
             .withType(GenerateMediaTestConfigurationTask::class.java)
             .names
-            .contains("support-media-test${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}")
+            .contains("support-media-test${GENERATE_TEST_CONFIGURATION_TASK}")
     ) {
         val task =
             parentProject.tasks.register(
-                "support-media-test${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}",
+                "support-media-test${GENERATE_TEST_CONFIGURATION_TASK}",
                 GenerateMediaTestConfigurationTask::class.java
             ) { task ->
                 AffectedModuleDetector.configureTaskGuard(task)
@@ -287,7 +391,7 @@
     } else {
         return parentProject.tasks
             .withType(GenerateMediaTestConfigurationTask::class.java)
-            .named("support-media-test${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}")
+            .named("support-media-test${GENERATE_TEST_CONFIGURATION_TASK}")
     }
 }
 
@@ -519,3 +623,11 @@
 
 private fun Project.isPrivacySandboxEnabled(): Boolean =
     extensions.findByType(ApplicationExtension::class.java)?.privacySandbox?.enable ?: false
+
+private const val COPY_TEST_APKS_TASK = "CopyTestApks"
+private const val GENERATE_PRIVACY_SANDBOX_MAIN_TEST_CONFIGURATION_TASK =
+    "GeneratePrivacySandboxMainTestConfiguration"
+private const val GENERATE_PRIVACY_SANDBOX_COMPAT_TEST_CONFIGURATION_TASK =
+    "GeneratePrivacySandboxCompatTestConfiguration"
+private const val GENERATE_TEST_CONFIGURATION_TASK = "GenerateTestConfiguration"
+private const val PRIVACY_SANDBOX_MIN_API_LEVEL = 34
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml
index edefeed..e50341f 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-fa/strings.xml
@@ -175,7 +175,7 @@
     <string name="notification_template_demo_title" msgid="5076051497316030274">"الگوی اعلان نمونه"</string>
     <string name="nav_map_template_demo_title" msgid="344985380763975398">"نمونه الگوی ناوبری فقط با نقشه"</string>
     <string name="nav_demos_title" msgid="72781206086461004">"نمونه‌های ناوبری"</string>
-    <string name="navigation_alert_title" msgid="8306554249264200848">"باز هم دام سرعت وجود دارد؟"</string>
+    <string name="navigation_alert_title" msgid="8306554249264200848">"باز هم تله سرعت وجود دارد؟"</string>
     <string name="navigation_alert_subtitle" msgid="3331130131492672264">"۱۰ دقیقه پیش گزارش شده است"</string>
     <string name="no_toll_card_permission" msgid="6789073114449712090">"اجازه کارت عوارض اعطا نشده است."</string>
     <string name="no_energy_level_permission" msgid="1684773185095107825">"اجازه میزان سوخت اعطا نشده است."</string>
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index c5a1983..c7fc05d 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -388,11 +388,11 @@
 
 package androidx.compose.foundation.gestures {
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface AnchoredDragScope {
+  public interface AnchoredDragScope {
     method public void dragTo(float newOffset, optional float lastKnownVelocity);
   }
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final class AnchoredDraggableDefaults {
+  public final class AnchoredDraggableDefaults {
     method @androidx.compose.runtime.Composable public <T> androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior(androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec);
     method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> getDecayAnimationSpec();
     method public kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> getPositionalThreshold();
@@ -408,14 +408,16 @@
     method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.foundation.gestures.AnchoredDraggableState<T> AnchoredDraggableState(T initialValue, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, kotlin.jvm.functions.Function0<java.lang.Float> velocityThreshold, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.foundation.gestures.DraggableAnchors<T> DraggableAnchors(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.gestures.DraggableAnchorsConfig<T>,kotlin.Unit> builder);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+    method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? animateTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? animateToWithDecay(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, float velocity, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, kotlin.coroutines.Continuation<? super java.lang.Float>);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? snapTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+    method public static suspend <T> Object? animateTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public static suspend <T> Object? animateToWithDecay(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, float velocity, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, kotlin.coroutines.Continuation<? super java.lang.Float>);
+    method public static suspend <T> Object? snapTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
   }
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public final class AnchoredDraggableState<T> {
-    ctor @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public AnchoredDraggableState(T initialValue, androidx.compose.foundation.gestures.DraggableAnchors<T> anchors, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
+  @androidx.compose.runtime.Stable public final class AnchoredDraggableState<T> {
+    ctor public AnchoredDraggableState(T initialValue, androidx.compose.foundation.gestures.DraggableAnchors<T> anchors, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
     ctor public AnchoredDraggableState(T initialValue, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
     method public suspend Object? anchoredDrag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.AnchoredDragScope,? super androidx.compose.foundation.gestures.DraggableAnchors<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? anchoredDrag(T targetValue, optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function4<? super androidx.compose.foundation.gestures.AnchoredDragScope,? super androidx.compose.foundation.gestures.DraggableAnchors<T>,? super T,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -452,7 +454,7 @@
 
   public static final class AnchoredDraggableState.Companion {
     method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public <T> androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.gestures.AnchoredDraggableState<T>,T> Saver(androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, kotlin.jvm.functions.Function0<java.lang.Float> velocityThreshold, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public <T> androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.gestures.AnchoredDraggableState<T>,T> Saver(optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
+    method public <T> androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.gestures.AnchoredDraggableState<T>,T> Saver(optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface BringIntoViewSpec {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index d9c60c2..88f35f2 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -390,11 +390,11 @@
 
 package androidx.compose.foundation.gestures {
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface AnchoredDragScope {
+  public interface AnchoredDragScope {
     method public void dragTo(float newOffset, optional float lastKnownVelocity);
   }
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public final class AnchoredDraggableDefaults {
+  public final class AnchoredDraggableDefaults {
     method @androidx.compose.runtime.Composable public <T> androidx.compose.foundation.gestures.TargetedFlingBehavior flingBehavior(androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec);
     method public androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> getDecayAnimationSpec();
     method public kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> getPositionalThreshold();
@@ -410,14 +410,16 @@
     method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.foundation.gestures.AnchoredDraggableState<T> AnchoredDraggableState(T initialValue, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, kotlin.jvm.functions.Function0<java.lang.Float> velocityThreshold, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.foundation.gestures.DraggableAnchors<T> DraggableAnchors(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.gestures.DraggableAnchorsConfig<T>,kotlin.Unit> builder);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+    method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.foundation.OverscrollEffect? overscrollEffect, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? animateTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? animateToWithDecay(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, float velocity, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, kotlin.coroutines.Continuation<? super java.lang.Float>);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static suspend <T> Object? snapTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public static <T> androidx.compose.ui.Modifier anchoredDraggable(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.AnchoredDraggableState<T> state, boolean reverseDirection, androidx.compose.foundation.gestures.Orientation orientation, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional boolean startDragImmediately, optional androidx.compose.foundation.gestures.FlingBehavior? flingBehavior);
+    method public static suspend <T> Object? animateTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public static suspend <T> Object? animateToWithDecay(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, float velocity, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, optional androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, kotlin.coroutines.Continuation<? super java.lang.Float>);
+    method public static suspend <T> Object? snapTo(androidx.compose.foundation.gestures.AnchoredDraggableState<T>, T targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit>);
   }
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public final class AnchoredDraggableState<T> {
-    ctor @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public AnchoredDraggableState(T initialValue, androidx.compose.foundation.gestures.DraggableAnchors<T> anchors, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
+  @androidx.compose.runtime.Stable public final class AnchoredDraggableState<T> {
+    ctor public AnchoredDraggableState(T initialValue, androidx.compose.foundation.gestures.DraggableAnchors<T> anchors, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
     ctor public AnchoredDraggableState(T initialValue, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
     method public suspend Object? anchoredDrag(optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.AnchoredDragScope,? super androidx.compose.foundation.gestures.DraggableAnchors<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? anchoredDrag(T targetValue, optional androidx.compose.foundation.MutatePriority dragPriority, kotlin.jvm.functions.Function4<? super androidx.compose.foundation.gestures.AnchoredDragScope,? super androidx.compose.foundation.gestures.DraggableAnchors<T>,? super T,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,? extends java.lang.Object?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -454,7 +456,7 @@
 
   public static final class AnchoredDraggableState.Companion {
     method @Deprecated @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public <T> androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.gestures.AnchoredDraggableState<T>,T> Saver(androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> decayAnimationSpec, kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> positionalThreshold, kotlin.jvm.functions.Function0<java.lang.Float> velocityThreshold, optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public <T> androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.gestures.AnchoredDraggableState<T>,T> Saver(optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
+    method public <T> androidx.compose.runtime.saveable.Saver<androidx.compose.foundation.gestures.AnchoredDraggableState<T>,T> Saver(optional kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> confirmValueChange);
   }
 
   @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface BringIntoViewSpec {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
index c13241a..60cf8e8 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldTest.kt
@@ -1447,6 +1447,21 @@
         }
     }
 
+    @Test
+    fun longText_doesNotCrash() {
+        var textLayoutProvider: (() -> TextLayoutResult?)? = null
+        inputMethodInterceptor.setTextFieldTestContent {
+            BasicTextField(
+                rememberTextFieldState("A".repeat(100_000)),
+                onTextLayout = { textLayoutProvider = it }
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(textLayoutProvider?.invoke()?.layoutInput?.text?.length).isEqualTo(100_000)
+        }
+    }
+
     private fun requestFocus(tag: String) = rule.onNodeWithTag(tag).requestFocus()
 
     private fun assertTextSelection(expected: TextRange) {
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
index 2fc1d32..d18722d 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCacheTest.kt
@@ -341,4 +341,17 @@
             )
         assertThat(actual.height).isEqualTo(expected.height)
     }
+
+    @Test
+    fun hugeString_doesntCrash() {
+        val text = "A".repeat(100_000)
+        val subject =
+            MultiParagraphLayoutCache(
+                    text = AnnotatedString(text),
+                    style = TextStyle(fontSize = 100.sp),
+                    fontFamilyResolver = fontFamilyResolver,
+                )
+                .also { it.density = density }
+        subject.layoutWithConstraints(Constraints(), LayoutDirection.Ltr)
+    }
 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt
index 18acdfe..6f75020 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCacheTest.kt
@@ -328,6 +328,15 @@
         assertThat(subject.slowCreateTextLayoutResultOrNull(style = style)).isNotNull()
     }
 
+    @Test
+    fun hugeString_doesntCrash() {
+        val text = "A".repeat(100_000)
+        val style = createTextStyle(fontSize = 100.sp)
+        val subject =
+            ParagraphLayoutCache(text, style, fontFamilyResolver).also { it.density = density }
+        subject.layoutWithConstraints(Constraints(), LayoutDirection.Ltr)
+    }
+
     private fun createTextStyle(
         fontSize: TextUnit,
         letterSpacing: TextUnit = TextUnit.Unspecified
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
index b21b1cb..4708dcc 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.interaction.FocusInteraction
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.relocation.findBringIntoViewParent
 import androidx.compose.foundation.relocation.scrollIntoView
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
@@ -41,6 +42,7 @@
 import androidx.compose.ui.node.findNearestAncestor
 import androidx.compose.ui.node.invalidateSemantics
 import androidx.compose.ui.node.observeReads
+import androidx.compose.ui.node.requireLayoutCoordinates
 import androidx.compose.ui.platform.InspectableModifier
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.debugInspectorInfo
@@ -198,7 +200,11 @@
         if (isFocused == wasFocused) return
         if (isFocused) {
             onFocus?.invoke()
-            coroutineScope.launch { scrollIntoView() }
+            val parent = findBringIntoViewParent()
+            if (parent != null) {
+                val layoutCoordinates = requireLayoutCoordinates()
+                coroutineScope.launch { parent.scrollIntoView(layoutCoordinates) }
+            }
             val pinnableContainer = retrievePinnableContainer()
             pinnedHandle = pinnableContainer?.pin()
             notifyObserverWhenAttached()
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index 3a8a68a..0ebc2d1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -90,7 +90,102 @@
  * @param state The associated [AnchoredDraggableState].
  * @param reverseDirection Whether to reverse the direction of the drag, so a top to bottom drag
  *   will behave like bottom to top, and a left to right drag will behave like right to left. If not
- *   specified, this will be determined based on [orientation] and [LocalLayoutDirection].
+ *   specified, this will be determined based on [orientation] and [LocalLayoutDirection] through
+ *   the other [anchoredDraggable] overload.
+ * @param orientation The orientation in which the [anchoredDraggable] can be dragged.
+ * @param enabled Whether this [anchoredDraggable] is enabled and should react to the user's input.
+ * @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
+ *   [Modifier.draggable].
+ * @param startDragImmediately when set to false, [draggable] will start dragging only when the
+ *   gesture crosses the touchSlop. This is useful to prevent users from "catching" an animating
+ *   widget when pressing on it. See [draggable] to learn more about startDragImmediately.
+ * @param flingBehavior Optionally configure how the anchored draggable performs the fling. By
+ *   default (if passing in null), this will snap to the closest anchor considering the velocity
+ *   thresholds and positional thresholds. See [AnchoredDraggableDefaults.flingBehavior].
+ */
+@OptIn(ExperimentalFoundationApi::class)
+fun <T> Modifier.anchoredDraggable(
+    state: AnchoredDraggableState<T>,
+    reverseDirection: Boolean,
+    orientation: Orientation,
+    enabled: Boolean = true,
+    interactionSource: MutableInteractionSource? = null,
+    startDragImmediately: Boolean = state.isAnimationRunning,
+    flingBehavior: FlingBehavior? = null
+): Modifier =
+    this then
+        AnchoredDraggableElement(
+            state = state,
+            orientation = orientation,
+            enabled = enabled,
+            reverseDirection = reverseDirection,
+            interactionSource = interactionSource,
+            overscrollEffect = null,
+            startDragImmediately = startDragImmediately,
+            flingBehavior = flingBehavior
+        )
+
+/**
+ * Enable drag gestures between a set of predefined values.
+ *
+ * When a drag is detected, the offset of the [AnchoredDraggableState] will be updated with the drag
+ * delta. If the [orientation] is set to [Orientation.Horizontal] and [LocalLayoutDirection]'s value
+ * is [LayoutDirection.Rtl], the drag deltas will be reversed. You should use this offset to move
+ * your content accordingly (see [Modifier.offset]). When the drag ends, the offset will be animated
+ * to one of the anchors and when that anchor is reached, the value of the [AnchoredDraggableState]
+ * will also be updated to the value corresponding to the new anchor.
+ *
+ * Dragging is constrained between the minimum and maximum anchors.
+ *
+ * @param state The associated [AnchoredDraggableState].
+ * @param orientation The orientation in which the [anchoredDraggable] can be dragged.
+ * @param enabled Whether this [anchoredDraggable] is enabled and should react to the user's input.
+ * @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
+ *   [Modifier.draggable].
+ * @param startDragImmediately when set to false, [draggable] will start dragging only when the
+ *   gesture crosses the touchSlop. This is useful to prevent users from "catching" an animating
+ *   widget when pressing on it. See [draggable] to learn more about startDragImmediately.
+ * @param flingBehavior Optionally configure how the anchored draggable performs the fling. By
+ *   default (if passing in null), this will snap to the closest anchor considering the velocity
+ *   thresholds and positional thresholds. See [AnchoredDraggableDefaults.flingBehavior].
+ */
+@OptIn(ExperimentalFoundationApi::class)
+fun <T> Modifier.anchoredDraggable(
+    state: AnchoredDraggableState<T>,
+    orientation: Orientation,
+    enabled: Boolean = true,
+    interactionSource: MutableInteractionSource? = null,
+    startDragImmediately: Boolean = state.isAnimationRunning,
+    flingBehavior: FlingBehavior? = null
+): Modifier =
+    this then
+        AnchoredDraggableElement(
+            state = state,
+            orientation = orientation,
+            enabled = enabled,
+            reverseDirection = null,
+            interactionSource = interactionSource,
+            overscrollEffect = null,
+            startDragImmediately = startDragImmediately,
+            flingBehavior = flingBehavior
+        )
+
+/**
+ * Enable drag gestures between a set of predefined values.
+ *
+ * When a drag is detected, the offset of the [AnchoredDraggableState] will be updated with the drag
+ * delta. You should use this offset to move your content accordingly (see [Modifier.offset]). When
+ * the drag ends, the offset will be animated to one of the anchors and when that anchor is reached,
+ * the value of the [AnchoredDraggableState] will also be updated to the value corresponding to the
+ * new anchor.
+ *
+ * Dragging is constrained between the minimum and maximum anchors.
+ *
+ * @param state The associated [AnchoredDraggableState].
+ * @param reverseDirection Whether to reverse the direction of the drag, so a top to bottom drag
+ *   will behave like bottom to top, and a left to right drag will behave like right to left. If not
+ *   specified, this will be determined based on [orientation] and [LocalLayoutDirection] through
+ *   the other [anchoredDraggable] overload.
  * @param orientation The orientation in which the [anchoredDraggable] can be dragged.
  * @param enabled Whether this [anchoredDraggable] is enabled and should react to the user's input.
  * @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal
@@ -105,6 +200,8 @@
  * @param flingBehavior Optionally configure how the anchored draggable performs the fling. By
  *   default (if passing in null), this will snap to the closest anchor considering the velocity
  *   thresholds and positional thresholds. See [AnchoredDraggableDefaults.flingBehavior].
+ *
+ * This API is experimental as it uses [OverscrollEffect], which is experimental.
  */
 @ExperimentalFoundationApi
 fun <T> Modifier.anchoredDraggable(
@@ -156,6 +253,8 @@
  * @param flingBehavior Optionally configure how the anchored draggable performs the fling. By
  *   default (if passing in null), this will snap to the closest anchor considering the velocity
  *   thresholds and positional thresholds. See [AnchoredDraggableDefaults.flingBehavior].
+ *
+ * This API is experimental as it uses [OverscrollEffect], which is experimental.
  */
 @ExperimentalFoundationApi
 fun <T> Modifier.anchoredDraggable(
@@ -543,7 +642,6 @@
  * @see [AnchoredDraggableState.anchoredDrag] to learn how to start the anchored drag and get the
  *   access to this scope.
  */
-@ExperimentalFoundationApi
 interface AnchoredDragScope {
     /**
      * Assign a new value for an offset value for [AnchoredDraggableState].
@@ -646,8 +744,8 @@
  * @param initialValue The initial value of the state.
  * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
  */
+@OptIn(ExperimentalFoundationApi::class) // DraggableAnchors is still experimental
 @Stable
-@ExperimentalFoundationApi
 class AnchoredDraggableState<T>(
     initialValue: T,
     internal val confirmValueChange: (newValue: T) -> Boolean = { true }
@@ -661,7 +759,6 @@
      * @param confirmValueChange Optional callback invoked to confirm or veto a pending state
      *   change.
      */
-    @ExperimentalFoundationApi
     constructor(
         initialValue: T,
         anchors: DraggableAnchors<T>,
@@ -1086,7 +1183,6 @@
 
     companion object {
         /** The default [Saver] implementation for [AnchoredDraggableState]. */
-        @ExperimentalFoundationApi
         fun <T : Any> Saver(confirmValueChange: (T) -> Boolean = { true }) =
             Saver<AnchoredDraggableState<T>, T>(
                 save = { it.currentValue },
@@ -1134,7 +1230,7 @@
  * @throws CancellationException if the interaction interrupted by another interaction like a
  *   gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
  */
-@ExperimentalFoundationApi
+@OptIn(ExperimentalFoundationApi::class)
 suspend fun <T> AnchoredDraggableState<T>.snapTo(targetValue: T) {
     anchoredDrag(targetValue = targetValue) { anchors, latestTarget ->
         val targetOffset = anchors.positionOf(latestTarget)
@@ -1177,7 +1273,7 @@
  * @throws CancellationException if the interaction interrupted by another interaction like a
  *   gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
  */
-@ExperimentalFoundationApi
+@OptIn(ExperimentalFoundationApi::class)
 suspend fun <T> AnchoredDraggableState<T>.animateTo(
     targetValue: T,
     animationSpec: AnimationSpec<Float> =
@@ -1209,7 +1305,7 @@
  * @throws CancellationException if the interaction interrupted bt another interaction like a
  *   gesture interaction or another programmatic interaction like [animateTo] or [snapTo] call.
  */
-@ExperimentalFoundationApi
+@OptIn(ExperimentalFoundationApi::class)
 suspend fun <T> AnchoredDraggableState<T>.animateToWithDecay(
     targetValue: T,
     velocity: Float,
@@ -1326,7 +1422,6 @@
 /**
  * Contains useful defaults for use with [AnchoredDraggableState] and [Modifier.anchoredDraggable]
  */
-@ExperimentalFoundationApi
 object AnchoredDraggableDefaults {
 
     /** The default spec for snapping, a tween spec */
@@ -1511,7 +1606,6 @@
  *   is invoked with.
  * @param snapAnimationSpec The animation spec that will be used to snap to a new state.
  */
-@ExperimentalFoundationApi
 internal fun <T> anchoredDraggableFlingBehavior(
     state: AnchoredDraggableState<T>,
     density: Density,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/ScrollIntoViewRequester.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/ScrollIntoViewRequester.kt
index 14efa18..dd43101 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/ScrollIntoViewRequester.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/ScrollIntoViewRequester.kt
@@ -21,6 +21,7 @@
 
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.toRect
+import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.node.DelegatableNode
 import androidx.compose.ui.node.requireLayoutCoordinates
 import androidx.compose.ui.unit.toSize
@@ -44,7 +45,14 @@
     if (!node.isAttached) return
     val layoutCoordinates = requireLayoutCoordinates()
     val parent = findBringIntoViewParent() ?: return
-    parent.bringChildIntoView(layoutCoordinates) {
+    parent.scrollIntoView(layoutCoordinates, rect)
+}
+
+internal suspend fun BringIntoViewParent.scrollIntoView(
+    layoutCoordinates: LayoutCoordinates,
+    rect: Rect? = null
+) {
+    bringChildIntoView(layoutCoordinates) {
         // If the rect is not specified, use a rectangle representing the entire composable.
         // If the coordinates are detached when this call is made, we don't bother even
         // submitting the request, but if the coordinates become detached while the request
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldTextLayoutModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldTextLayoutModifier.kt
index df3c97a..8a649d6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldTextLayoutModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldTextLayoutModifier.kt
@@ -145,7 +145,15 @@
                 constraints = constraints,
             )
 
-        val placeable = measurable.measure(Constraints.fixed(result.size.width, result.size.height))
+        val placeable =
+            measurable.measure(
+                Constraints.fitPrioritizingWidth(
+                    minWidth = result.size.width,
+                    maxWidth = result.size.width,
+                    minHeight = result.size.height,
+                    maxHeight = result.size.height
+                )
+            )
 
         // calculate the min height for single line text to prevent text cuts.
         // for single line text maxLines puts in max height constraint based on
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
index 1873ed3..3f475b2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
@@ -27,7 +27,7 @@
     overflow: TextOverflow,
     maxIntrinsicWidth: Float
 ): Constraints =
-    Constraints(
+    Constraints.fitPrioritizingWidth(
         minWidth = 0,
         maxWidth = finalMaxWidth(constraints, softWrap, overflow, maxIntrinsicWidth),
         minHeight = 0,
diff --git a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/ProgressIndicatorBenchmark.kt b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
index 9f707e7..da2078f 100644
--- a/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
+++ b/compose/material3/benchmark/src/androidTest/java/androidx/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
@@ -18,7 +18,7 @@
 
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.CircularWavyProgressIndicator
-import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.LinearProgressIndicator
 import androidx.compose.material3.LinearWavyProgressIndicator
 import androidx.compose.material3.MaterialTheme
@@ -77,7 +77,7 @@
     LayeredComposeTestCase(), ToggleableTestCase {
     private lateinit var state: MutableFloatState
 
-    @OptIn(ExperimentalMaterial3Api::class)
+    @OptIn(ExperimentalMaterial3ExpressiveApi::class)
     @Composable
     override fun MeasuredContent() {
         state = remember { mutableFloatStateOf(0f) }
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index efb3d49..2bc212b 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -1188,7 +1188,7 @@
     property public final long unselectedTextColor;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @kotlin.jvm.JvmInline public final value class NavigationItemIconPosition {
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @kotlin.jvm.JvmInline public final value class NavigationItemIconPosition {
     field public static final androidx.compose.material3.NavigationItemIconPosition.Companion Companion;
   }
 
@@ -1576,7 +1576,7 @@
     enum_constant public static final androidx.compose.material3.SheetValue PartiallyExpanded;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class ShortNavigationBarDefaults {
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ShortNavigationBarDefaults {
     method public int getArrangement();
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method @androidx.compose.runtime.Composable public long getContentColor();
@@ -1588,14 +1588,14 @@
     field public static final androidx.compose.material3.ShortNavigationBarDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class ShortNavigationBarItemDefaults {
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ShortNavigationBarItemDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material3.NavigationItemColors colors();
     field public static final androidx.compose.material3.ShortNavigationBarItemDefaults INSTANCE;
   }
 
   public final class ShortNavigationBarKt {
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ShortNavigationBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ShortNavigationBarItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ShortNavigationBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ShortNavigationBarItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
   public interface SingleChoiceSegmentedButtonRowScope extends androidx.compose.foundation.layout.RowScope {
@@ -2313,41 +2313,47 @@
     property public final androidx.compose.ui.text.TextStyle titleSmall;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class WavyProgressIndicatorDefaults {
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WavyProgressIndicatorDefaults {
     method public float getCircularContainerSize();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getCircularIndicatorStroke();
+    method public float getCircularIndicatorTrackGapSize();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getCircularTrackStroke();
     method public float getCircularWavelength();
     method public kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> getIndicatorAmplitude();
     method @androidx.compose.runtime.Composable public long getIndicatorColor();
-    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getIndicatorStroke();
-    method public float getIndicatorTrackGapSize();
     method public float getLinearContainerHeight();
     method public float getLinearContainerWidth();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getLinearIndicatorStroke();
+    method public float getLinearIndicatorTrackGapSize();
     method public float getLinearTrackStopIndicatorSize();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getLinearTrackStroke();
     method public float getLinearWavelength();
     method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getProgressAnimationSpec();
     method @androidx.compose.runtime.Composable public long getTrackColor();
-    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getTrackStroke();
     property public final float CircularContainerSize;
+    property public final float CircularIndicatorTrackGapSize;
     property public final float CircularWavelength;
-    property public final float IndicatorTrackGapSize;
     property public final float LinearContainerHeight;
     property public final float LinearContainerWidth;
+    property public final float LinearIndicatorTrackGapSize;
     property public final float LinearTrackStopIndicatorSize;
     property public final float LinearWavelength;
     property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> ProgressAnimationSpec;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke circularIndicatorStroke;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke circularTrackStroke;
     property public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> indicatorAmplitude;
     property @androidx.compose.runtime.Composable public final long indicatorColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke indicatorStroke;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke linearIndicatorStroke;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke linearTrackStroke;
     property @androidx.compose.runtime.Composable public final long trackColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke trackStroke;
     field public static final androidx.compose.material3.WavyProgressIndicatorDefaults INSTANCE;
   }
 
   public final class WavyProgressIndicatorKt {
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
   }
 
 }
@@ -2413,6 +2419,7 @@
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class PullToRefreshDefaults {
     method @androidx.compose.runtime.Composable public void Indicator(androidx.compose.material3.pulltorefresh.PullToRefreshState state, boolean isRefreshing, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long color, optional float threshold);
+    method @androidx.compose.runtime.Composable public void IndicatorBox(androidx.compose.material3.pulltorefresh.PullToRefreshState state, boolean isRefreshing, optional androidx.compose.ui.Modifier modifier, optional float threshold, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float elevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getElevation();
     method @androidx.compose.runtime.Composable public long getIndicatorColor();
@@ -2430,7 +2437,6 @@
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PullToRefreshBox(boolean isRefreshing, kotlin.jvm.functions.Function0<kotlin.Unit> onRefresh, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.pulltorefresh.PullToRefreshState state, optional androidx.compose.ui.Alignment contentAlignment, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> indicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.pulltorefresh.PullToRefreshState PullToRefreshState();
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.ui.Modifier pullToRefresh(androidx.compose.ui.Modifier, boolean isRefreshing, androidx.compose.material3.pulltorefresh.PullToRefreshState state, optional boolean enabled, optional float threshold, kotlin.jvm.functions.Function0<kotlin.Unit> onRefresh);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.ui.Modifier pullToRefreshIndicator(androidx.compose.ui.Modifier, androidx.compose.material3.pulltorefresh.PullToRefreshState state, boolean isRefreshing, optional float threshold, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float elevation);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.pulltorefresh.PullToRefreshState rememberPullToRefreshState();
   }
 
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index efb3d49..2bc212b 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -1188,7 +1188,7 @@
     property public final long unselectedTextColor;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @kotlin.jvm.JvmInline public final value class NavigationItemIconPosition {
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @kotlin.jvm.JvmInline public final value class NavigationItemIconPosition {
     field public static final androidx.compose.material3.NavigationItemIconPosition.Companion Companion;
   }
 
@@ -1576,7 +1576,7 @@
     enum_constant public static final androidx.compose.material3.SheetValue PartiallyExpanded;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class ShortNavigationBarDefaults {
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ShortNavigationBarDefaults {
     method public int getArrangement();
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method @androidx.compose.runtime.Composable public long getContentColor();
@@ -1588,14 +1588,14 @@
     field public static final androidx.compose.material3.ShortNavigationBarDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class ShortNavigationBarItemDefaults {
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class ShortNavigationBarItemDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material3.NavigationItemColors colors();
     field public static final androidx.compose.material3.ShortNavigationBarItemDefaults INSTANCE;
   }
 
   public final class ShortNavigationBarKt {
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ShortNavigationBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ShortNavigationBarItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ShortNavigationBar(optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional androidx.compose.foundation.layout.WindowInsets windowInsets, optional int arrangement, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void ShortNavigationBarItem(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> icon, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? badge, optional int iconPosition, optional androidx.compose.material3.NavigationItemColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource);
   }
 
   public interface SingleChoiceSegmentedButtonRowScope extends androidx.compose.foundation.layout.RowScope {
@@ -2313,41 +2313,47 @@
     property public final androidx.compose.ui.text.TextStyle titleSmall;
   }
 
-  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class WavyProgressIndicatorDefaults {
+  @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public final class WavyProgressIndicatorDefaults {
     method public float getCircularContainerSize();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getCircularIndicatorStroke();
+    method public float getCircularIndicatorTrackGapSize();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getCircularTrackStroke();
     method public float getCircularWavelength();
     method public kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> getIndicatorAmplitude();
     method @androidx.compose.runtime.Composable public long getIndicatorColor();
-    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getIndicatorStroke();
-    method public float getIndicatorTrackGapSize();
     method public float getLinearContainerHeight();
     method public float getLinearContainerWidth();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getLinearIndicatorStroke();
+    method public float getLinearIndicatorTrackGapSize();
     method public float getLinearTrackStopIndicatorSize();
+    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getLinearTrackStroke();
     method public float getLinearWavelength();
     method public androidx.compose.animation.core.AnimationSpec<java.lang.Float> getProgressAnimationSpec();
     method @androidx.compose.runtime.Composable public long getTrackColor();
-    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.drawscope.Stroke getTrackStroke();
     property public final float CircularContainerSize;
+    property public final float CircularIndicatorTrackGapSize;
     property public final float CircularWavelength;
-    property public final float IndicatorTrackGapSize;
     property public final float LinearContainerHeight;
     property public final float LinearContainerWidth;
+    property public final float LinearIndicatorTrackGapSize;
     property public final float LinearTrackStopIndicatorSize;
     property public final float LinearWavelength;
     property public final androidx.compose.animation.core.AnimationSpec<java.lang.Float> ProgressAnimationSpec;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke circularIndicatorStroke;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke circularTrackStroke;
     property public final kotlin.jvm.functions.Function1<java.lang.Float,java.lang.Float> indicatorAmplitude;
     property @androidx.compose.runtime.Composable public final long indicatorColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke indicatorStroke;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke linearIndicatorStroke;
+    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke linearTrackStroke;
     property @androidx.compose.runtime.Composable public final long trackColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.drawscope.Stroke trackStroke;
     field public static final androidx.compose.material3.WavyProgressIndicatorDefaults INSTANCE;
   }
 
   public final class WavyProgressIndicatorKt {
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void CircularWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float wavelength);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void LinearWavyProgressIndicator(kotlin.jvm.functions.Function0<java.lang.Float> progress, optional androidx.compose.ui.Modifier modifier, optional long color, optional long trackColor, optional androidx.compose.ui.graphics.drawscope.Stroke stroke, optional androidx.compose.ui.graphics.drawscope.Stroke trackStroke, optional float gapSize, optional float stopSize, optional kotlin.jvm.functions.Function1<? super java.lang.Float,java.lang.Float> amplitude, optional float wavelength, optional float waveSpeed);
   }
 
 }
@@ -2413,6 +2419,7 @@
 
   @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public final class PullToRefreshDefaults {
     method @androidx.compose.runtime.Composable public void Indicator(androidx.compose.material3.pulltorefresh.PullToRefreshState state, boolean isRefreshing, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long color, optional float threshold);
+    method @androidx.compose.runtime.Composable public void IndicatorBox(androidx.compose.material3.pulltorefresh.PullToRefreshState state, boolean isRefreshing, optional androidx.compose.ui.Modifier modifier, optional float threshold, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float elevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method public float getElevation();
     method @androidx.compose.runtime.Composable public long getIndicatorColor();
@@ -2430,7 +2437,6 @@
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void PullToRefreshBox(boolean isRefreshing, kotlin.jvm.functions.Function0<kotlin.Unit> onRefresh, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.pulltorefresh.PullToRefreshState state, optional androidx.compose.ui.Alignment contentAlignment, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> indicator, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.material3.pulltorefresh.PullToRefreshState PullToRefreshState();
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.ui.Modifier pullToRefresh(androidx.compose.ui.Modifier, boolean isRefreshing, androidx.compose.material3.pulltorefresh.PullToRefreshState state, optional boolean enabled, optional float threshold, kotlin.jvm.functions.Function0<kotlin.Unit> onRefresh);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api public static androidx.compose.ui.Modifier pullToRefreshIndicator(androidx.compose.ui.Modifier, androidx.compose.material3.pulltorefresh.PullToRefreshState state, boolean isRefreshing, optional float threshold, optional androidx.compose.ui.graphics.Shape shape, optional long containerColor, optional float elevation);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static androidx.compose.material3.pulltorefresh.PullToRefreshState rememberPullToRefreshState();
   }
 
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 008272d..6741b9e 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -124,6 +124,7 @@
 import androidx.compose.material3.samples.PlainTooltipWithManualInvocationSample
 import androidx.compose.material3.samples.PrimaryIconTabs
 import androidx.compose.material3.samples.PrimaryTextTabs
+import androidx.compose.material3.samples.PullToRefreshCustomIndicatorWithDefaultTransform
 import androidx.compose.material3.samples.PullToRefreshSample
 import androidx.compose.material3.samples.PullToRefreshSampleCustomState
 import androidx.compose.material3.samples.PullToRefreshScalingSample
@@ -149,6 +150,8 @@
 import androidx.compose.material3.samples.SecondaryTextTabs
 import androidx.compose.material3.samples.SegmentedButtonMultiSelectSample
 import androidx.compose.material3.samples.SegmentedButtonSingleSelectSample
+import androidx.compose.material3.samples.ShortNavigationBarSample
+import androidx.compose.material3.samples.ShortNavigationBarWithHorizontalItemsSample
 import androidx.compose.material3.samples.SimpleBottomAppBar
 import androidx.compose.material3.samples.SimpleBottomSheetScaffoldSample
 import androidx.compose.material3.samples.SimpleCenterAlignedTopAppBar
@@ -859,6 +862,20 @@
 val NavigationBarExamples =
     listOf(
         Example(
+            name = ::ShortNavigationBarSample.name,
+            description = NavigationBarExampleDescription,
+            sourceUrl = NavigationBarExampleSourceUrl,
+        ) {
+            ShortNavigationBarSample()
+        },
+        Example(
+            name = ::ShortNavigationBarWithHorizontalItemsSample.name,
+            description = NavigationBarExampleDescription,
+            sourceUrl = NavigationBarExampleSourceUrl,
+        ) {
+            ShortNavigationBarWithHorizontalItemsSample()
+        },
+        Example(
             name = ::NavigationBarSample.name,
             description = NavigationBarExampleDescription,
             sourceUrl = NavigationBarExampleSourceUrl,
@@ -1051,6 +1068,13 @@
         ) {
             PullToRefreshViewModelSample()
         },
+        Example(
+            name = ::PullToRefreshViewModelSample.name,
+            description = PullToRefreshExampleDescription,
+            sourceUrl = PullToRefreshExampleSourceUrl
+        ) {
+            PullToRefreshCustomIndicatorWithDefaultTransform()
+        },
     )
 
 private const val RadioButtonsExampleDescription = "Radio buttons examples"
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationBarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationBarSamples.kt
index 36502a5..2c76d18 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationBarSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/NavigationBarSamples.kt
@@ -17,18 +17,79 @@
 package androidx.compose.material3.samples
 
 import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.Icon
 import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarArrangement
 import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.NavigationItemIconPosition
+import androidx.compose.material3.ShortNavigationBar
+import androidx.compose.material3.ShortNavigationBarItem
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun ShortNavigationBarSample() {
+    var selectedItem by remember { mutableIntStateOf(0) }
+    val items = listOf("Songs", "Artists", "Playlists")
+
+    ShortNavigationBar {
+        items.forEachIndexed { index, item ->
+            ShortNavigationBarItem(
+                icon = { Icon(Icons.Filled.Favorite, contentDescription = item) },
+                label = { Text(item) },
+                selected = selectedItem == index,
+                onClick = { selectedItem = index }
+            )
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
+fun ShortNavigationBarWithHorizontalItemsSample() {
+    var selectedItem by remember { mutableIntStateOf(0) }
+    val items = listOf("Songs", "Artists", "Playlists")
+
+    Column {
+        Text(
+            "Note: this is configuration is better displayed in medium screen sizes.",
+            Modifier.padding(16.dp)
+        )
+
+        Spacer(Modifier.height(32.dp))
+
+        ShortNavigationBar(arrangement = NavigationBarArrangement.Centered) {
+            items.forEachIndexed { index, item ->
+                ShortNavigationBarItem(
+                    iconPosition = NavigationItemIconPosition.Start,
+                    icon = { Icon(Icons.Filled.Favorite, contentDescription = item) },
+                    label = { Text(item) },
+                    selected = selectedItem == index,
+                    onClick = { selectedItem = index }
+                )
+            }
+        }
+    }
+}
 
 @Preview
 @Sampled
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ProgressIndicatorSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ProgressIndicatorSamples.kt
index a1ae0e2..1891882 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ProgressIndicatorSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ProgressIndicatorSamples.kt
@@ -26,7 +26,7 @@
 import androidx.compose.foundation.layout.width
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.CircularWavyProgressIndicator
-import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.LinearProgressIndicator
 import androidx.compose.material3.LinearWavyProgressIndicator
 import androidx.compose.material3.MaterialTheme
@@ -73,7 +73,7 @@
     }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Preview
 @Sampled
 @Composable
@@ -100,7 +100,7 @@
     }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Preview
 @Sampled
 @Composable
@@ -143,7 +143,7 @@
     Column(horizontalAlignment = Alignment.CenterHorizontally) { LinearProgressIndicator() }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Preview
 @Sampled
 @Composable
@@ -175,7 +175,7 @@
     }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Preview
 @Sampled
 @Composable
@@ -200,7 +200,7 @@
     }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Preview
 @Sampled
 @Composable
@@ -241,7 +241,7 @@
     Column(horizontalAlignment = Alignment.CenterHorizontally) { CircularProgressIndicator() }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Preview
 @Sampled
 @Composable
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt
index 7e7f9dc..ea0ea7c 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/PullToRefreshSamples.kt
@@ -29,6 +29,7 @@
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
@@ -352,3 +353,59 @@
         }
     }
 }
+
+@Sampled
+@Composable
+@Preview
+@OptIn(ExperimentalMaterial3Api::class)
+fun PullToRefreshCustomIndicatorWithDefaultTransform() {
+    var itemCount by remember { mutableIntStateOf(15) }
+    var isRefreshing by remember { mutableStateOf(false) }
+    val state = rememberPullToRefreshState()
+    val coroutineScope = rememberCoroutineScope()
+    val onRefresh: () -> Unit = {
+        isRefreshing = true
+        coroutineScope.launch {
+            delay(1500)
+            itemCount += 5
+            isRefreshing = false
+        }
+    }
+
+    Scaffold(
+        topBar = {
+            TopAppBar(
+                title = { Text("Title") },
+                // Provide an accessible alternative to trigger refresh.
+                actions = {
+                    IconButton(onClick = onRefresh) {
+                        Icon(Icons.Filled.Refresh, "Trigger Refresh")
+                    }
+                }
+            )
+        }
+    ) {
+        PullToRefreshBox(
+            modifier = Modifier.padding(it),
+            state = state,
+            isRefreshing = isRefreshing,
+            onRefresh = onRefresh,
+            indicator = {
+                PullToRefreshDefaults.IndicatorBox(state = state, isRefreshing = isRefreshing) {
+                    if (isRefreshing) {
+                        CircularProgressIndicator(modifier = Modifier.fillMaxWidth())
+                    } else {
+                        CircularProgressIndicator(
+                            modifier = Modifier.fillMaxWidth(),
+                            progress = { state.distanceFraction }
+                        )
+                    }
+                }
+            }
+        ) {
+            LazyColumn(Modifier.fillMaxSize()) {
+                items(itemCount) { ListItem({ Text(text = "Item ${itemCount - it}") }) }
+            }
+        }
+    }
+}
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarScreenshotTest.kt
index 9c1d2d1..bf33073 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarScreenshotTest.kt
@@ -53,7 +53,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@@ -433,7 +433,7 @@
  * @param modifier the [Modifier] applied to the navigation bar
  * @param setUnselectedItemsAsDisabled when true, marks unselected items as disabled
  */
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 private fun DefaultShortNavigationBar(
     interactionSource: MutableInteractionSource,
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarTest.kt
index 48fc29d..e02d719 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ShortNavigationBarTest.kt
@@ -66,7 +66,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class ShortNavigationBarTest {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt
index 9d444f0..5852853 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorScreenshotTest.kt
@@ -44,7 +44,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt
index 865bc65..a8760b3 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/WavyProgressIndicatorTest.kt
@@ -48,7 +48,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class WavyProgressIndicatorTest {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
index 26ce3b8..8933e20 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
@@ -74,7 +74,7 @@
 
 /** Class that describes the different supported icon positions of the navigation item. */
 @JvmInline
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
 value class NavigationItemIconPosition private constructor(private val value: Int) {
     companion object {
         /* The icon is positioned on top of the label. */
@@ -226,7 +226,7 @@
  *   for this item. You can create and pass in your own `remember`ed instance to observe
  *   [Interaction]s and customize the appearance / behavior of this item in different states
  */
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 internal fun NavigationItem(
     selected: Boolean,
@@ -335,7 +335,7 @@
     }
 }
 
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 private fun NavigationItemLayout(
     interactionSource: InteractionSource,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
index abe68c7..7fead98 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ProgressIndicator.kt
@@ -33,6 +33,8 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.progressSemantics
 import androidx.compose.material3.ProgressIndicatorDefaults.drawStopIndicator
+import androidx.compose.material3.tokens.CircularProgressIndicatorTokens
+import androidx.compose.material3.tokens.LinearProgressIndicatorTokens
 import androidx.compose.material3.tokens.ProgressIndicatorTokens
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
@@ -863,7 +865,7 @@
         @Composable get() = Color.Transparent
 
     /** Default stroke width for a circular progress indicator. */
-    val CircularStrokeWidth: Dp = ProgressIndicatorTokens.TrackThickness
+    val CircularStrokeWidth: Dp = CircularProgressIndicatorTokens.TrackThickness
 
     /** Default stroke cap for a linear progress indicator. */
     val LinearStrokeCap: StrokeCap = StrokeCap.Round
@@ -878,19 +880,19 @@
     @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
     @get:ExperimentalMaterial3Api
     @ExperimentalMaterial3Api
-    val LinearTrackStopIndicatorSize: Dp = ProgressIndicatorTokens.StopSize
+    val LinearTrackStopIndicatorSize: Dp = LinearProgressIndicatorTokens.StopSize
 
     /** Default indicator track gap size for a linear progress indicator. */
     @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
     @get:ExperimentalMaterial3Api
     @ExperimentalMaterial3Api
-    val LinearIndicatorTrackGapSize: Dp = ProgressIndicatorTokens.ActiveTrackSpace
+    val LinearIndicatorTrackGapSize: Dp = LinearProgressIndicatorTokens.TrackActiveSpace
 
     /** Default indicator track gap size for a circular progress indicator. */
     @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
     @get:ExperimentalMaterial3Api
     @ExperimentalMaterial3Api
-    val CircularIndicatorTrackGapSize: Dp = ProgressIndicatorTokens.ActiveTrackSpace
+    val CircularIndicatorTrackGapSize: Dp = CircularProgressIndicatorTokens.TrackActiveSpace
 
     /**
      * The default [AnimationSpec] that should be used when animating between progress in a
@@ -955,13 +957,12 @@
 internal val LinearIndicatorWidth = 240.dp
 
 /*@VisibleForTesting*/
-internal val LinearIndicatorHeight = ProgressIndicatorTokens.TrackThickness
+internal val LinearIndicatorHeight = LinearProgressIndicatorTokens.Height
 
 // CircularProgressIndicator Material specs
 // Diameter of the indicator circle
 /*@VisibleForTesting*/
-internal val CircularIndicatorDiameter =
-    ProgressIndicatorTokens.Size - ProgressIndicatorTokens.TrackThickness * 2
+internal val CircularIndicatorDiameter = CircularProgressIndicatorTokens.Size
 
 // Indeterminate linear indicator transition specs
 
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ShortNavigationBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ShortNavigationBar.kt
index 2503b0c..e0fd8eb 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ShortNavigationBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ShortNavigationBar.kt
@@ -63,6 +63,14 @@
  *   [NavigationBarArrangement.Centered], so that the navigation items are distributed grouped on
  *   the center of the bar.
  *
+ * A simple example of the first configuration looks like this:
+ *
+ * @sample androidx.compose.material3.samples.ShortNavigationBarSample
+ *
+ * And of the second configuration:
+ *
+ * @sample androidx.compose.material3.samples.ShortNavigationBarWithHorizontalItemsSample
+ *
  * See [ShortNavigationBarItem] for configurations specific to each item, and not the overall
  * [ShortNavigationBar] component.
  *
@@ -74,7 +82,7 @@
  * @param arrangement the [NavigationBarArrangement] of this navigation bar
  * @param content the content of this navigation bar, typically [ShortNavigationBarItem]s
  */
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
 @Composable
 fun ShortNavigationBar(
     modifier: Modifier = Modifier,
@@ -173,7 +181,7 @@
  *   preview the item in different states. Note that if `null` is provided, interactions will still
  *   happen internally.
  */
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
 @Composable
 fun ShortNavigationBarItem(
     selected: Boolean,
@@ -227,7 +235,7 @@
 }
 
 /** Defaults used in [ShortNavigationBar]. */
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
 object ShortNavigationBarDefaults {
     /** Default container color for a short navigation bar. */
     // TODO: Replace with token.
@@ -253,7 +261,7 @@
 }
 
 /** Defaults used in [ShortNavigationBarItem]. */
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
 object ShortNavigationBarItemDefaults {
     /**
      * Creates a [NavigationItemColors] with the provided colors according to the Material
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt
index 73b8f6c..1f35152 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/WavyProgressIndicator.kt
@@ -29,9 +29,12 @@
 import androidx.compose.animation.core.rememberInfiniteTransition
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.requiredSizeIn
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.progressSemantics
 import androidx.compose.material3.internal.toPath
+import androidx.compose.material3.tokens.CircularProgressIndicatorTokens
+import androidx.compose.material3.tokens.LinearProgressIndicatorTokens
 import androidx.compose.material3.tokens.MotionTokens
 import androidx.compose.material3.tokens.ProgressIndicatorTokens
 import androidx.compose.runtime.Composable
@@ -123,17 +126,16 @@
  *
  * @sample androidx.compose.material3.samples.LinearThickWavyProgressIndicatorSample
  */
-// TODO: Mark with expressive experimental annotation
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
 @Composable
 fun LinearWavyProgressIndicator(
     progress: () -> Float,
     modifier: Modifier = Modifier,
     color: Color = WavyProgressIndicatorDefaults.indicatorColor,
     trackColor: Color = WavyProgressIndicatorDefaults.trackColor,
-    stroke: Stroke = WavyProgressIndicatorDefaults.indicatorStroke,
-    trackStroke: Stroke = WavyProgressIndicatorDefaults.trackStroke,
-    gapSize: Dp = WavyProgressIndicatorDefaults.IndicatorTrackGapSize,
+    stroke: Stroke = WavyProgressIndicatorDefaults.linearIndicatorStroke,
+    trackStroke: Stroke = WavyProgressIndicatorDefaults.linearTrackStroke,
+    gapSize: Dp = WavyProgressIndicatorDefaults.LinearIndicatorTrackGapSize,
     stopSize: Dp = WavyProgressIndicatorDefaults.LinearTrackStopIndicatorSize,
     amplitude: (progress: Float) -> Float = WavyProgressIndicatorDefaults.indicatorAmplitude,
     wavelength: Dp = WavyProgressIndicatorDefaults.LinearWavelength,
@@ -182,6 +184,7 @@
             .semantics(mergeDescendants = true) {
                 progressBarRangeInfo = ProgressBarRangeInfo(coercedProgress(), 0f..1f)
             }
+            .requiredSizeIn(minWidth = LinearContainerMinWidth)
             .size(
                 width = WavyProgressIndicatorDefaults.LinearContainerWidth,
                 height = WavyProgressIndicatorDefaults.LinearContainerHeight
@@ -282,16 +285,15 @@
  * @param wavelength the length of a wave
  * @sample androidx.compose.material3.samples.IndeterminateLinearWavyProgressIndicatorSample
  */
-// TODO: Mark with expressive experimental annotation
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
 @Composable
 fun LinearWavyProgressIndicator(
     modifier: Modifier = Modifier,
     color: Color = WavyProgressIndicatorDefaults.indicatorColor,
     trackColor: Color = WavyProgressIndicatorDefaults.trackColor,
-    stroke: Stroke = WavyProgressIndicatorDefaults.indicatorStroke,
-    trackStroke: Stroke = stroke,
-    gapSize: Dp = WavyProgressIndicatorDefaults.IndicatorTrackGapSize,
+    stroke: Stroke = WavyProgressIndicatorDefaults.linearIndicatorStroke,
+    trackStroke: Stroke = WavyProgressIndicatorDefaults.linearTrackStroke,
+    gapSize: Dp = WavyProgressIndicatorDefaults.LinearIndicatorTrackGapSize,
     wavelength: Dp = WavyProgressIndicatorDefaults.LinearWavelength
 ) {
     val infiniteTransition = rememberInfiniteTransition()
@@ -371,6 +373,7 @@
         modifier
             .then(IncreaseSemanticsBounds)
             .progressSemantics()
+            .requiredSizeIn(minWidth = LinearContainerMinWidth)
             .size(
                 WavyProgressIndicatorDefaults.LinearContainerWidth,
                 WavyProgressIndicatorDefaults.LinearContainerHeight
@@ -450,17 +453,16 @@
  *
  * @sample androidx.compose.material3.samples.CircularThickWavyProgressIndicatorSample
  */
-// TODO: Mark with expressive experimental annotation
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
 @Composable
 fun CircularWavyProgressIndicator(
     progress: () -> Float,
     modifier: Modifier = Modifier,
     color: Color = WavyProgressIndicatorDefaults.indicatorColor,
     trackColor: Color = WavyProgressIndicatorDefaults.trackColor,
-    stroke: Stroke = WavyProgressIndicatorDefaults.indicatorStroke,
-    trackStroke: Stroke = WavyProgressIndicatorDefaults.trackStroke,
-    gapSize: Dp = WavyProgressIndicatorDefaults.IndicatorTrackGapSize,
+    stroke: Stroke = WavyProgressIndicatorDefaults.circularIndicatorStroke,
+    trackStroke: Stroke = WavyProgressIndicatorDefaults.circularTrackStroke,
+    gapSize: Dp = WavyProgressIndicatorDefaults.CircularIndicatorTrackGapSize,
     amplitude: (progress: Float) -> Float = WavyProgressIndicatorDefaults.indicatorAmplitude,
     wavelength: Dp = WavyProgressIndicatorDefaults.CircularWavelength,
     waveSpeed: Dp = wavelength // Match to 1 wavelength per second
@@ -567,16 +569,15 @@
  *   wavelength may be different to ensure a continuous wave shape.
  * @sample androidx.compose.material3.samples.IndeterminateCircularWavyProgressIndicatorSample
  */
-// TODO: Mark with expressive experimental annotation
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
 @Composable
 fun CircularWavyProgressIndicator(
     modifier: Modifier = Modifier,
     color: Color = WavyProgressIndicatorDefaults.indicatorColor,
     trackColor: Color = WavyProgressIndicatorDefaults.trackColor,
-    stroke: Stroke = WavyProgressIndicatorDefaults.indicatorStroke,
-    trackStroke: Stroke = WavyProgressIndicatorDefaults.trackStroke,
-    gapSize: Dp = WavyProgressIndicatorDefaults.IndicatorTrackGapSize,
+    stroke: Stroke = WavyProgressIndicatorDefaults.circularIndicatorStroke,
+    trackStroke: Stroke = WavyProgressIndicatorDefaults.circularTrackStroke,
+    gapSize: Dp = WavyProgressIndicatorDefaults.CircularIndicatorTrackGapSize,
     wavelength: Dp = WavyProgressIndicatorDefaults.CircularWavelength
 ) {
     val circularShapes = remember { CircularShapes() }
@@ -920,8 +921,7 @@
 }
 
 /** Contains the default values used for wavy progress indicators */
-// TODO: Mark with expressive experimental annotation
-@ExperimentalMaterial3Api
+@ExperimentalMaterial3ExpressiveApi
 object WavyProgressIndicatorDefaults {
 
     /**
@@ -942,50 +942,83 @@
     val trackColor: Color
         @Composable get() = ProgressIndicatorTokens.TrackColor.value
 
-    /** A default active indicator [Stroke]. */
-    val indicatorStroke: Stroke
+    /** A default linear progress indicator active indicator [Stroke]. */
+    val linearIndicatorStroke: Stroke
         @Composable
         get() =
             Stroke(
                 width =
                     with(LocalDensity.current) {
-                        4.dp.toPx() // TODO: Link to a token value for a thin PI
+                        LinearProgressIndicatorTokens.ActiveThickness.toPx()
                     },
                 cap = StrokeCap.Round
             )
 
-    /** A default track [Stroke]. */
-    val trackStroke: Stroke
+    /** A default circular progress indicator active indicator [Stroke]. */
+    val circularIndicatorStroke: Stroke
         @Composable
         get() =
             Stroke(
                 width =
                     with(LocalDensity.current) {
-                        4.dp.toPx() // TODO: Link to a token value for a thin PI
+                        CircularProgressIndicatorTokens.ActiveThickness.toPx()
+                    },
+                cap = StrokeCap.Round
+            )
+
+    /** A default linear progress indicator track [Stroke]. */
+    val linearTrackStroke: Stroke
+        @Composable
+        get() =
+            Stroke(
+                width =
+                    with(LocalDensity.current) {
+                        LinearProgressIndicatorTokens.TrackThickness.toPx()
+                    },
+                cap = StrokeCap.Round
+            )
+
+    /** A default circular progress indicator track [Stroke]. */
+    val circularTrackStroke: Stroke
+        @Composable
+        get() =
+            Stroke(
+                width =
+                    with(LocalDensity.current) {
+                        CircularProgressIndicatorTokens.TrackThickness.toPx()
                     },
                 cap = StrokeCap.Round
             )
 
     /** A default wavelength of a linear progress indicator when it's in a wavy form. */
-    val LinearWavelength: Dp = 40.dp // TODO: Link to a token value
+    val LinearWavelength: Dp = LinearProgressIndicatorTokens.ActiveWaveWavelength
 
     /** A default linear progress indicator container height. */
-    val LinearContainerHeight: Dp = 10.dp // TODO: Link to a token value
+    val LinearContainerHeight: Dp = LinearProgressIndicatorTokens.WaveHeight
 
     /** A default linear progress indicator container width. */
-    val LinearContainerWidth: Dp = 240.dp // TODO: Link to a token value
+    val LinearContainerWidth: Dp = 240.dp
 
     /** A default linear stop indicator size. */
-    val LinearTrackStopIndicatorSize: Dp = 4.dp // TODO: Link to a token value
+    val LinearTrackStopIndicatorSize: Dp = LinearProgressIndicatorTokens.StopSize
 
     /** A default circular progress indicator container size. */
-    val CircularContainerSize: Dp = 48.dp // TODO: Link to a token value
+    val CircularContainerSize: Dp = CircularProgressIndicatorTokens.WaveSize
 
     /** A default wavelength of a circular progress indicator when it's in a wavy form. */
-    val CircularWavelength: Dp = 15.dp // TODO: Link to a token value
+    val CircularWavelength: Dp = CircularProgressIndicatorTokens.ActiveWaveWavelength
 
-    /** A default gap size that appears in between the active indicator and the track. */
-    val IndicatorTrackGapSize: Dp = 4.dp // TODO: Link to a token value
+    /**
+     * A default gap size that appears in between the active indicator and the track at the linear
+     * progress indicator.
+     */
+    val LinearIndicatorTrackGapSize: Dp = LinearProgressIndicatorTokens.TrackActiveSpace
+
+    /**
+     * A default gap size that appears in between the active indicator and the track at the circular
+     * progress indicator.
+     */
+    val CircularIndicatorTrackGapSize: Dp = CircularProgressIndicatorTokens.TrackActiveSpace
 
     /** A function that returns the indicator's amplitude for a given progress */
     val indicatorAmplitude: (progress: Float) -> Float = { progress ->
@@ -1844,6 +1877,10 @@
     }
 }
 
+// Set the linear indicator min width to the smallest circular indicator value at the tokens. Small
+// linear indicators should be substituted with circular ones.
+private val LinearContainerMinWidth = CircularProgressIndicatorTokens.Size
+
 // Total duration for one linear cycle
 private const val LinearAnimationDuration = 1750
 
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
index 1ec27e6..65d22ea 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/pulltorefresh/PullToRefresh.kt
@@ -104,6 +104,10 @@
  * Scaling behavior can be implemented like this
  *
  * @sample androidx.compose.material3.samples.PullToRefreshScalingSample
+ *
+ * Custom indicators with default transforms can be seen in
+ *
+ * @sample androidx.compose.material3.samples.PullToRefreshCustomIndicatorWithDefaultTransform
  * @param isRefreshing whether a refresh is occurring
  * @param onRefresh callback invoked when the user gesture crosses the threshold, thereby requesting
  *   a refresh.
@@ -142,49 +146,6 @@
 }
 
 /**
- * A Modifier that handles the size, offset, clipping, shadow, and background drawing of a
- * pull-to-refresh indicator, useful when implementing custom indicators.
- * [PullToRefreshDefaults.Indicator] applies this automatically.
- *
- * @param state the state of this modifier, will use `state.distanceFraction` and [threshold] to
- *   calculate the offset
- * @param isRefreshing whether a refresh is occurring
- * @param threshold how much the indicator can be pulled down before a refresh is triggered on
- *   release
- * @param shape the [Shape] of this indicator
- * @param containerColor the container color of this indicator
- * @param elevation the elevation for the indicator
- */
-@ExperimentalMaterial3Api
-fun Modifier.pullToRefreshIndicator(
-    state: PullToRefreshState,
-    isRefreshing: Boolean,
-    threshold: Dp = PullToRefreshDefaults.PositionalThreshold,
-    shape: Shape = PullToRefreshDefaults.shape,
-    containerColor: Color = Color.Unspecified,
-    elevation: Dp = PullToRefreshDefaults.Elevation,
-): Modifier =
-    this.size(SpinnerContainerSize)
-        .drawWithContent {
-            clipRect(
-                top = 0f,
-                left = -Float.MAX_VALUE,
-                right = Float.MAX_VALUE,
-                bottom = Float.MAX_VALUE
-            ) {
-                [email protected]()
-            }
-        }
-        .graphicsLayer {
-            val showElevation = state.distanceFraction > 0f || isRefreshing
-            translationY = state.distanceFraction * threshold.roundToPx() - size.height
-            shadowElevation = if (showElevation) elevation.toPx() else 0f
-            this.shape = shape
-            clip = true
-        }
-        .background(color = containerColor, shape = shape)
-
-/**
  * A Modifier that adds nested scroll to a container to support a pull-to-refresh gesture. When the
  * user pulls a distance greater than [threshold] and releases the gesture, [onRefresh] is invoked.
  * [PullToRefreshBox] applies this automatically.
@@ -281,13 +242,7 @@
 
     override fun onAttach() {
         delegate(nestedScrollNode)
-        coroutineScope.launch {
-            if (isRefreshing) {
-                state.snapTo(1f)
-            } else {
-                state.snapTo(0f)
-            }
-        }
+        coroutineScope.launch { state.snapTo(if (isRefreshing) 1f else 0f) }
     }
 
     override fun onPreScroll(
@@ -425,28 +380,90 @@
     /** The default refresh threshold for [rememberPullToRefreshState] */
     val PositionalThreshold = 80.dp
 
-    /** The default elevation for [pullToRefreshIndicator] */
+    /** The default elevation for [IndicatorBox] */
     val Elevation = ElevationTokens.Level2
 
-    /** The default indicator for [PullToRefreshBox]. */
+    /**
+     * A Wrapper that handles the size, offset, clipping, shadow, and background drawing for a
+     * pull-to-refresh indicator, useful when implementing custom indicators.
+     * [PullToRefreshDefaults.Indicator] uses this as the container.
+     *
+     * @param state the state of this modifier, will use `state.distanceFraction` and [threshold] to
+     *   calculate the offset
+     * @param isRefreshing whether a refresh is occurring
+     * @param modifier the modifier applied to this layout
+     * @param threshold how much the indicator can be pulled down before a refresh is triggered on
+     *   release
+     * @param shape the [Shape] of this indicator
+     * @param containerColor the container color of this indicator
+     * @param elevation the elevation for the indicator
+     * @param content content for this [IndicatorBox]
+     */
+    @Composable
+    fun IndicatorBox(
+        state: PullToRefreshState,
+        isRefreshing: Boolean,
+        modifier: Modifier = Modifier,
+        threshold: Dp = PositionalThreshold,
+        shape: Shape = PullToRefreshDefaults.shape,
+        containerColor: Color = Color.Unspecified,
+        elevation: Dp = Elevation,
+        content: @Composable BoxScope.() -> Unit
+    ) {
+        Box(
+            modifier =
+                modifier
+                    .size(SpinnerContainerSize)
+                    .drawWithContent {
+                        clipRect(
+                            top = 0f,
+                            left = -Float.MAX_VALUE,
+                            right = Float.MAX_VALUE,
+                            bottom = Float.MAX_VALUE
+                        ) {
+                            [email protected]()
+                        }
+                    }
+                    .graphicsLayer {
+                        val showElevation = state.distanceFraction > 0f || isRefreshing
+                        translationY = state.distanceFraction * threshold.roundToPx() - size.height
+                        shadowElevation = if (showElevation) elevation.toPx() else 0f
+                        this.shape = shape
+                        clip = true
+                    }
+                    .background(color = containerColor, shape = shape),
+            contentAlignment = Alignment.Center,
+            content = content
+        )
+    }
+
+    /**
+     * The default indicator for [PullToRefreshBox].
+     *
+     * @param state the state of this modifier, will use `state.distanceFraction` and [threshold] to
+     *   calculate the offset
+     * @param isRefreshing whether a refresh is occurring
+     * @param modifier the modifier applied to this layout
+     * @param containerColor the container color of this indicator
+     * @param color the color of this indicator
+     * @param threshold how much the indicator can be pulled down before a refresh is triggered on
+     *   release
+     */
     @Composable
     fun Indicator(
         state: PullToRefreshState,
         isRefreshing: Boolean,
         modifier: Modifier = Modifier,
-        containerColor: Color = PullToRefreshDefaults.containerColor,
-        color: Color = PullToRefreshDefaults.indicatorColor,
+        containerColor: Color = this.containerColor,
+        color: Color = this.indicatorColor,
         threshold: Dp = PositionalThreshold,
     ) {
-        Box(
-            modifier =
-                modifier.pullToRefreshIndicator(
-                    state = state,
-                    isRefreshing = isRefreshing,
-                    containerColor = containerColor,
-                    threshold = threshold,
-                ),
-            contentAlignment = Alignment.Center
+        IndicatorBox(
+            modifier = modifier,
+            state = state,
+            isRefreshing = isRefreshing,
+            containerColor = containerColor,
+            threshold = threshold,
         ) {
             Crossfade(
                 targetState = isRefreshing,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/CircularProgressIndicatorTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/CircularProgressIndicatorTokens.kt
new file mode 100644
index 0000000..9ed1149
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/CircularProgressIndicatorTokens.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_4_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object CircularProgressIndicatorTokens {
+    val ActiveThickness = 4.0.dp
+    val ActiveWaveAmplitude = 2.0.dp
+    val ActiveWaveWavelength = 15.0.dp
+    val Size = 40.0.dp
+    val TrackActiveSpace = 4.0.dp
+    val TrackThickness = 4.0.dp
+    val WaveSize = 48.0.dp
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LinearProgressIndicatorTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LinearProgressIndicatorTokens.kt
new file mode 100644
index 0000000..d0838d3
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/LinearProgressIndicatorTokens.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// VERSION: v0_4_0
+// GENERATED CODE - DO NOT MODIFY BY HAND
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object LinearProgressIndicatorTokens {
+    val ActiveThickness = 4.0.dp
+    val ActiveWaveAmplitude = 3.0.dp
+    val ActiveWaveWavelength = 40.0.dp
+    val Height = 4.0.dp
+    val StopSize = 4.0.dp
+    val StopTrailingSpace = 0.0.dp
+    val TrackActiveSpace = 4.0.dp
+    val TrackThickness = 4.0.dp
+    val WaveHeight = 10.0.dp
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ProgressIndicatorTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ProgressIndicatorTokens.kt
index ade192d..eea7701 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ProgressIndicatorTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/ProgressIndicatorTokens.kt
@@ -13,23 +13,16 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-// Version: v2_3_5
+// VERSION: v0_4_0
 // GENERATED CODE - DO NOT MODIFY BY HAND
 
 package androidx.compose.material3.tokens
 
-import androidx.compose.ui.unit.dp
-
 internal object ProgressIndicatorTokens {
     val ActiveIndicatorColor = ColorSchemeKeyTokens.Primary
     val ActiveShape = ShapeKeyTokens.CornerFull
-    val ActiveThickness = 4.0.dp
-    val ActiveTrackSpace = 4.0.dp
     val StopColor = ColorSchemeKeyTokens.Primary
-    val StopShape = 4.0.dp
-    val StopSize = 4.0.dp
+    val StopShape = ShapeKeyTokens.CornerFull
     val TrackColor = ColorSchemeKeyTokens.SecondaryContainer
     val TrackShape = ShapeKeyTokens.CornerFull
-    val TrackThickness = 4.0.dp
-    val Size = 48.0.dp
 }
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
index aa33218..180831d 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/TextMeasurerTest.kt
@@ -367,6 +367,12 @@
             .isEqualTo(TextDecoration.LineThrough)
     }
 
+    @Test
+    fun emptyConstraints_hugeString_dontCrash() {
+        val subject = textMeasurer()
+        subject.measure("A".repeat(100_000), TextStyle.Default)
+    }
+
     private fun textLayoutInput(
         text: AnnotatedString = AnnotatedString("Hello"),
         style: TextStyle = TextStyle.Default,
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
index b9e30ed..47d2651 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextMeasurer.kt
@@ -325,7 +325,12 @@
                     MultiParagraph(
                         intrinsics = nonNullIntrinsics,
                         constraints =
-                            Constraints(maxWidth = width, maxHeight = constraints.maxHeight),
+                            Constraints.fitPrioritizingWidth(
+                                minWidth = 0,
+                                maxWidth = width,
+                                minHeight = 0,
+                                maxHeight = constraints.maxHeight
+                            ),
                         // This is a fallback behavior for ellipsis. Native
                         maxLines = finalMaxLines,
                         ellipsis = overflow == TextOverflow.Ellipsis
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index b2fe7f6..9529deb 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -1958,7 +1958,7 @@
     method @RequiresApi(api=android.os.Build.VERSION_CODES.Q) public static <T> java.util.List<T!> readParcelableList(android.os.Parcel, java.util.List<T!>, ClassLoader?, Class<T!>);
     method public static <T extends java.io.Serializable> T? readSerializable(android.os.Parcel, ClassLoader?, Class<T!>);
     method public static <T> android.util.SparseArray<T!>? readSparseArray(android.os.Parcel, ClassLoader?, Class<? extends T!>);
-    method @Deprecated public static void writeBoolean(android.os.Parcel, boolean);
+    method public static void writeBoolean(android.os.Parcel, boolean);
   }
 
   @Deprecated public final class ParcelableCompat {
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 6d8746a..c280485 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -2348,7 +2348,7 @@
     method @RequiresApi(api=android.os.Build.VERSION_CODES.Q) public static <T> java.util.List<T!> readParcelableList(android.os.Parcel, java.util.List<T!>, ClassLoader?, Class<T!>);
     method public static <T extends java.io.Serializable> T? readSerializable(android.os.Parcel, ClassLoader?, Class<T!>);
     method public static <T> android.util.SparseArray<T!>? readSparseArray(android.os.Parcel, ClassLoader?, Class<? extends T!>);
-    method @Deprecated public static void writeBoolean(android.os.Parcel, boolean);
+    method public static void writeBoolean(android.os.Parcel, boolean);
   }
 
   @Deprecated public final class ParcelableCompat {
diff --git a/core/core/src/main/java/androidx/core/os/ParcelCompat.java b/core/core/src/main/java/androidx/core/os/ParcelCompat.java
index fb67665..52de196 100644
--- a/core/core/src/main/java/androidx/core/os/ParcelCompat.java
+++ b/core/core/src/main/java/androidx/core/os/ParcelCompat.java
@@ -50,15 +50,13 @@
     /**
      * Write a boolean value into the parcel at the current f{@link Parcel#dataPosition()},
      * growing {@link Parcel#dataCapacity()} if needed.
-     *
-     * <p>Note: This method currently delegates to {@link Parcel#writeInt} with a value of 1 or 0
-     * for true or false, respectively, but may change in the future.
-     * @deprecated Call {@link Parcel#writeInt()} directly.
      */
-    @Deprecated
-    @androidx.annotation.ReplaceWith(expression = "out.writeInt(value ? 1 : 0)")
     public static void writeBoolean(@NonNull Parcel out, boolean value) {
-        out.writeInt(value ? 1 : 0);
+        if (Build.VERSION.SDK_INT >= 29) {
+            Api29Impl.writeBoolean(out, value);
+        } else {
+            out.writeInt(value ? 1 : 0);
+        }
     }
 
     /**
@@ -406,6 +404,11 @@
                 @NonNull List<T> list, @Nullable ClassLoader cl) {
             return in.readParcelableList(list, cl);
         }
+
+        @DoNotInline
+        static void writeBoolean(@NonNull Parcel parcel, boolean val) {
+            parcel.writeBoolean(val);
+        }
     }
 
     @RequiresApi(30)
diff --git a/core/core/src/main/res/values-pa/strings.xml b/core/core/src/main/res/values-pa/strings.xml
index c24092c..6099d51 100644
--- a/core/core/src/main/res/values-pa/strings.xml
+++ b/core/core/src/main/res/values-pa/strings.xml
@@ -20,7 +20,7 @@
     <string name="status_bar_notification_info_overflow" msgid="6277540029070332960">"999+"</string>
     <string name="call_notification_answer_action" msgid="881409763997275156">"ਜਵਾਬ ਦਿਓ"</string>
     <string name="call_notification_answer_video_action" msgid="8793775615905189152">"ਵੀਡੀਓ"</string>
-    <string name="call_notification_decline_action" msgid="3229508546291798546">"ਅਸਵੀਕਾਰ ਕਰੋ"</string>
+    <string name="call_notification_decline_action" msgid="3229508546291798546">"ਕਾਲ ਕੱਟੋ"</string>
     <string name="call_notification_hang_up_action" msgid="2659457946726154263">"ਸਮਾਪਤ ਕਰੋ"</string>
     <string name="call_notification_incoming_text" msgid="6107532579223922871">"ਇਨਕਮਿੰਗ ਕਾਲ"</string>
     <string name="call_notification_ongoing_text" msgid="8623827134497363134">"ਜਾਰੀ ਕਾਲ"</string>
diff --git a/libraryversions.toml b/libraryversions.toml
index d781128..a084055 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -162,10 +162,10 @@
 WEAR_INPUT_TESTING = "1.2.0-alpha03"
 WEAR_ONGOING = "1.1.0-alpha02"
 WEAR_PHONE_INTERACTIONS = "1.1.0-alpha04"
-WEAR_PROTOLAYOUT = "1.2.0-alpha04"
+WEAR_PROTOLAYOUT = "1.2.0-alpha05"
 WEAR_PROTOLAYOUT_MATERIAL3 = "1.0.0-alpha01"
 WEAR_REMOTE_INTERACTIONS = "1.1.0-alpha02"
-WEAR_TILES = "1.4.0-alpha04"
+WEAR_TILES = "1.4.0-alpha05"
 WEAR_TOOLING_PREVIEW = "1.0.0-rc01"
 WEAR_WATCHFACE = "1.3.0-alpha03"
 WEBKIT = "1.12.0-alpha02"
diff --git a/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/BaseWrongStartDestinationTypeDetector.kt b/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/BaseWrongStartDestinationTypeDetector.kt
index 32055f6..ffa3358 100644
--- a/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/BaseWrongStartDestinationTypeDetector.kt
+++ b/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/BaseWrongStartDestinationTypeDetector.kt
@@ -25,16 +25,8 @@
 import com.android.tools.lint.detector.api.Severity
 import com.android.tools.lint.detector.api.SourceCodeScanner
 import com.intellij.psi.PsiMethod
-import org.jetbrains.kotlin.analysis.api.analyze
-import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
-import org.jetbrains.kotlin.idea.references.mainReference
-import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
-import org.jetbrains.kotlin.psi.KtExpression
-import org.jetbrains.kotlin.psi.KtReferenceExpression
 import org.jetbrains.uast.UCallExpression
 import org.jetbrains.uast.UElement
-import org.jetbrains.uast.UQualifiedReferenceExpression
-import org.jetbrains.uast.USimpleNameReferenceExpression
 import org.jetbrains.uast.getParameterForArgument
 
 abstract class BaseWrongStartDestinationTypeDetector(
@@ -54,42 +46,7 @@
                 parameterNames.contains(node.getParameterForArgument(it)?.name)
             } ?: return
 
-        /**
-         * True if:
-         * 1. reference to object (i.e. val myStart = TestStart(), startDest = myStart)
-         * 2. object declaration (i.e. object MyStart, startDest = MyStart)
-         * 3. class reference (i.e. class MyStart, startDest = MyStart)
-         *
-         *    We only want to catch case 3., so we need more filters to eliminate case 1 & 2.
-         */
-        val isSimpleRefExpression = startNode is USimpleNameReferenceExpression
-
-        /** True if nested class i.e. OuterClass.InnerClass */
-        val isQualifiedRefExpression = startNode is UQualifiedReferenceExpression
-
-        if (!(isSimpleRefExpression || isQualifiedRefExpression)) return
-
-        val sourcePsi = startNode.sourcePsi as? KtExpression ?: return
-        val (isClassType, name) =
-            analyze(sourcePsi) {
-                val symbol =
-                    when (sourcePsi) {
-                        is KtDotQualifiedExpression -> {
-                            val lastChild = sourcePsi.lastChild
-                            if (lastChild is KtReferenceExpression) {
-                                lastChild.mainReference.resolveToSymbol()
-                            } else {
-                                null
-                            }
-                        }
-                        is KtReferenceExpression -> sourcePsi.mainReference.resolveToSymbol()
-                        else -> null
-                    }
-                        as? KtClassOrObjectSymbol ?: return
-
-                symbol.classKind.isClass to symbol.name
-            }
-
+        val (isClassType, name) = startNode.isClassReference()
         if (isClassType) {
             context.report(
                 getIssue(),
diff --git a/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/LintUtil.kt b/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/LintUtil.kt
new file mode 100644
index 0000000..d393265
--- /dev/null
+++ b/navigation/navigation-common-lint/src/main/java/androidx/navigation/common/lint/LintUtil.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.navigation.common.lint
+
+import org.jetbrains.kotlin.analysis.api.analyze
+import org.jetbrains.kotlin.analysis.api.symbols.KtClassOrObjectSymbol
+import org.jetbrains.kotlin.idea.references.mainReference
+import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
+import org.jetbrains.kotlin.psi.KtExpression
+import org.jetbrains.kotlin.psi.KtReferenceExpression
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UQualifiedReferenceExpression
+import org.jetbrains.uast.USimpleNameReferenceExpression
+
+/** Catches simple class/interface name reference */
+fun UExpression.isClassReference(): Pair<Boolean, String?> {
+    /**
+     * True if:
+     * 1. reference to object (i.e. val myStart = TestStart(), startDest = myStart)
+     * 2. object declaration (i.e. object MyStart, startDest = MyStart)
+     * 3. class reference (i.e. class MyStart, startDest = MyStart)
+     *
+     *    We only want to catch case 3., so we need more filters to eliminate case 1 & 2.
+     */
+    val isSimpleRefExpression = this is USimpleNameReferenceExpression
+
+    /** True if nested class i.e. OuterClass.InnerClass */
+    val isQualifiedRefExpression = this is UQualifiedReferenceExpression
+
+    if (!(isSimpleRefExpression || isQualifiedRefExpression)) return false to null
+
+    val sourcePsi = sourcePsi as? KtExpression ?: return false to null
+    return analyze(sourcePsi) {
+        val symbol =
+            when (sourcePsi) {
+                is KtDotQualifiedExpression -> {
+                    val lastChild = sourcePsi.lastChild
+                    if (lastChild is KtReferenceExpression) {
+                        lastChild.mainReference.resolveToSymbol()
+                    } else {
+                        null
+                    }
+                }
+                is KtReferenceExpression -> sourcePsi.mainReference.resolveToSymbol()
+                else -> null
+            }
+                as? KtClassOrObjectSymbol ?: return false to null
+
+        (symbol.classKind.isClass || symbol.classKind.name == "INTERFACE") to
+            symbol.name?.asString()
+    }
+}
diff --git a/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/WrongStartDestinationTypeDetectorTest.kt b/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/WrongStartDestinationTypeDetectorTest.kt
index 606e2b3..647068d 100644
--- a/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/WrongStartDestinationTypeDetectorTest.kt
+++ b/navigation/navigation-common-lint/src/test/java/androidx/navigation/common/lint/WrongStartDestinationTypeDetectorTest.kt
@@ -132,6 +132,8 @@
                     builder.navigation<TestGraph>(startDestination = Outer.InnerClass)
                     builder.navigation<TestGraph>(startDestination = InterfaceChildClass)
                     builder.navigation<TestGraph>(startDestination = AbstractChildClass)
+                    builder.navigation<TestGraph>(startDestination = TestInterface)
+                    builder.navigation<TestGraph>(startDestination = TestAbstract)
                 }
                 """
                     )
@@ -171,7 +173,19 @@
 you can also pass in its KClass reference AbstractChildClass::class [WrongStartDestinationType]
     builder.navigation<TestGraph>(startDestination = AbstractChildClass)
                                                      ~~~~~~~~~~~~~~~~~~
-5 errors, 0 warnings
+src/com/example/test.kt:13: Error: StartDestination should not be a simple class name reference.
+Did you mean to call its constructor TestInterface(...)?
+If the class TestInterface does not contain arguments,
+you can also pass in its KClass reference TestInterface::class [WrongStartDestinationType]
+    builder.navigation<TestGraph>(startDestination = TestInterface)
+                                                     ~~~~~~~~~~~~~
+src/com/example/test.kt:14: Error: StartDestination should not be a simple class name reference.
+Did you mean to call its constructor TestAbstract(...)?
+If the class TestAbstract does not contain arguments,
+you can also pass in its KClass reference TestAbstract::class [WrongStartDestinationType]
+    builder.navigation<TestGraph>(startDestination = TestAbstract)
+                                                     ~~~~~~~~~~~~
+7 errors, 0 warnings
             """
             )
     }
@@ -252,6 +266,8 @@
                     provider.navigation(startDestination = Outer.InnerClass) {}
                     provider.navigation(startDestination = InterfaceChildClass) {}
                     provider.navigation(startDestination = AbstractChildClass) {}
+                    provider.navigation(startDestination = TestInterface)
+                    provider.navigation(startDestination = TestAbstract)
                 }
                 """
                     )
@@ -291,7 +307,19 @@
 you can also pass in its KClass reference AbstractChildClass::class [WrongStartDestinationType]
     provider.navigation(startDestination = AbstractChildClass) {}
                                            ~~~~~~~~~~~~~~~~~~
-5 errors, 0 warnings
+src/com/example/test.kt:12: Error: StartDestination should not be a simple class name reference.
+Did you mean to call its constructor TestInterface(...)?
+If the class TestInterface does not contain arguments,
+you can also pass in its KClass reference TestInterface::class [WrongStartDestinationType]
+    provider.navigation(startDestination = TestInterface)
+                                           ~~~~~~~~~~~~~
+src/com/example/test.kt:13: Error: StartDestination should not be a simple class name reference.
+Did you mean to call its constructor TestAbstract(...)?
+If the class TestAbstract does not contain arguments,
+you can also pass in its KClass reference TestAbstract::class [WrongStartDestinationType]
+    provider.navigation(startDestination = TestAbstract)
+                                           ~~~~~~~~~~~~
+7 errors, 0 warnings
             """
             )
     }
@@ -372,6 +400,8 @@
                     navGraph.setStartDestination(startDestRoute = Outer.InnerClass)
                     navGraph.setStartDestination(startDestRoute = InterfaceChildClass)
                     navGraph.setStartDestination(startDestRoute = AbstractChildClass)
+                    navGraph.setStartDestination(startDestRoute = TestInterface)
+                    navGraph.setStartDestination(startDestRoute = InterfaceChildClass)
                 }
                 """
                     )
@@ -411,7 +441,19 @@
 you can also pass in its KClass reference AbstractChildClass::class [WrongStartDestinationType]
     navGraph.setStartDestination(startDestRoute = AbstractChildClass)
                                                   ~~~~~~~~~~~~~~~~~~
-5 errors, 0 warnings
+src/com/example/test.kt:12: Error: StartDestination should not be a simple class name reference.
+Did you mean to call its constructor TestInterface(...)?
+If the class TestInterface does not contain arguments,
+you can also pass in its KClass reference TestInterface::class [WrongStartDestinationType]
+    navGraph.setStartDestination(startDestRoute = TestInterface)
+                                                  ~~~~~~~~~~~~~
+src/com/example/test.kt:13: Error: StartDestination should not be a simple class name reference.
+Did you mean to call its constructor InterfaceChildClass(...)?
+If the class InterfaceChildClass does not contain arguments,
+you can also pass in its KClass reference InterfaceChildClass::class [WrongStartDestinationType]
+    navGraph.setStartDestination(startDestRoute = InterfaceChildClass)
+                                                  ~~~~~~~~~~~~~~~~~~~
+7 errors, 0 warnings
             """
             )
     }
diff --git a/navigation/navigation-runtime-lint/src/main/java/androidx/navigation/runtime/lint/NavigationRuntimeIssueRegistry.kt b/navigation/navigation-runtime-lint/src/main/java/androidx/navigation/runtime/lint/NavigationRuntimeIssueRegistry.kt
index 80e2a36..bc11285 100644
--- a/navigation/navigation-runtime-lint/src/main/java/androidx/navigation/runtime/lint/NavigationRuntimeIssueRegistry.kt
+++ b/navigation/navigation-runtime-lint/src/main/java/androidx/navigation/runtime/lint/NavigationRuntimeIssueRegistry.kt
@@ -33,6 +33,7 @@
             listOf(
                 DeepLinkInActivityDestinationDetector.DeepLinkInActivityDestination,
                 WrongStartDestinationTypeDetector.WrongStartDestinationType,
+                WrongNavigateRouteDetector.WrongNavigateRouteType,
             )
 
     override val vendor =
diff --git a/navigation/navigation-runtime-lint/src/main/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetector.kt b/navigation/navigation-runtime-lint/src/main/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetector.kt
new file mode 100644
index 0000000..ee38a50
--- /dev/null
+++ b/navigation/navigation-runtime-lint/src/main/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetector.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.navigation.runtime.lint
+
+import androidx.navigation.common.lint.isClassReference
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UClassLiteralExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.getParameterForArgument
+
+class WrongNavigateRouteDetector() : Detector(), SourceCodeScanner {
+
+    companion object {
+        val WrongNavigateRouteType =
+            Issue.create(
+                id = "WrongNavigateRouteType",
+                briefDescription =
+                    "Navigation route should be an object literal or a destination class instance " +
+                        "with arguments.",
+                explanation =
+                    "If the destination class contains arguments, the route is " +
+                        "expected to be class instance with the arguments filled in.",
+                category = Category.CORRECTNESS,
+                severity = Severity.ERROR,
+                implementation =
+                    Implementation(WrongNavigateRouteDetector::class.java, Scope.JAVA_FILE_SCOPE)
+            )
+    }
+
+    final override fun getApplicableMethodNames(): List<String> = listOf("navigate")
+
+    final override fun visitMethodCall(
+        context: JavaContext,
+        node: UCallExpression,
+        method: PsiMethod
+    ) {
+        val startNode =
+            node.valueArguments.find { node.getParameterForArgument(it)?.name == "route" } ?: return
+
+        val isClassLiteral = startNode is UClassLiteralExpression
+        val (isClassType, _) = startNode.isClassReference()
+        if (isClassType || isClassLiteral) {
+            context.report(
+                WrongNavigateRouteType,
+                startNode,
+                context.getNameLocation(startNode as UElement),
+                """
+                The route should be a destination class instance or destination object.
+                    """
+                    .trimIndent()
+            )
+        }
+    }
+}
diff --git a/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetectorTest.kt b/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetectorTest.kt
new file mode 100644
index 0000000..3931fb0
--- /dev/null
+++ b/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/WrongNavigateRouteDetectorTest.kt
@@ -0,0 +1,451 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.navigation.runtime.lint
+
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestMode
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+class WrongNavigateRouteDetectorTest : LintDetectorTest() {
+
+    override fun getDetector(): Detector = WrongNavigateRouteDetector()
+
+    override fun getIssues(): MutableList<Issue> =
+        mutableListOf(WrongNavigateRouteDetector.WrongNavigateRouteType)
+
+    @Test
+    fun testEmptyConstructorNoError() {
+        lint()
+            .files(
+                kotlin(
+                        """
+                package com.example
+
+                import androidx.navigation.*
+
+                fun createGraph() {
+                    val navController = NavController()
+                    navController.navigate(route = TestClass())
+                }
+                """
+                    )
+                    .indented(),
+                byteCode,
+            )
+            .skipTestModes(TestMode.FULLY_QUALIFIED)
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun testNoError() {
+        lint()
+            .files(
+                kotlin(
+                        """
+                package com.example
+
+                import androidx.navigation.*
+
+                fun createGraph() {
+                    val navController = NavController()
+                    navController.navigate(route = TestClassWithArg(10))
+                    navController.navigate(route = TestObject)
+                    navController.navigate(route = classInstanceRef)
+                    navController.navigate(route = classInstanceWithArgRef)
+                    navController.navigate(route = Outer)
+                    navController.navigate(route = Outer.InnerObject)
+                    navController.navigate(route = Outer.InnerClass(123))
+                    navController.navigate(route = 123)
+                    navController.navigate(route = "www.test.com/{arg}")
+                    navController.navigate(route = InterfaceChildClass(true))
+                    navController.navigate(route = InterfaceChildObject)
+                    navController.navigate(route = AbstractChildClass(true))
+                    navController.navigate(route = AbstractChildObject)
+                }
+                """
+                    )
+                    .indented(),
+                byteCode,
+            )
+            .skipTestModes(TestMode.FULLY_QUALIFIED)
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun testHasError() {
+        lint()
+            .files(
+                kotlin(
+                        """
+                package com.example
+
+                import androidx.navigation.*
+
+                fun createGraph() {
+                    val navController = NavController()
+                    navController.navigate(route = TestClass)
+                    navController.navigate(route = TestClass::class)
+                    navController.navigate(route = TestClassWithArg)
+                    navController.navigate(route = TestClassWithArg::class)
+                    navController.navigate(route = TestInterface)
+                    navController.navigate(route = TestInterface::class)
+                    navController.navigate(route = InterfaceChildClass)
+                    navController.navigate(route = InterfaceChildClass::class)
+                    navController.navigate(route = TestAbstract)
+                    navController.navigate(route = TestAbstract::class)
+                    navController.navigate(route = AbstractChildClass)
+                    navController.navigate(route = AbstractChildClass::class)
+                    navController.navigate(route = InterfaceChildClass::class)
+                    navController.navigate(route = Outer.InnerClass)
+                    navController.navigate(route = Outer.InnerClass::class)
+                }
+                """
+                    )
+                    .indented(),
+                byteCode,
+            )
+            .skipTestModes(TestMode.FULLY_QUALIFIED)
+            .run()
+            .expect(
+                """
+src/com/example/test.kt:7: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+    navController.navigate(route = TestClass)
+                                   ~~~~~~~~~
+src/com/example/test.kt:8: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+    navController.navigate(route = TestClass::class)
+                                   ~~~~~~~~~~~~~~~~
+src/com/example/test.kt:9: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+    navController.navigate(route = TestClassWithArg)
+                                   ~~~~~~~~~~~~~~~~
+src/com/example/test.kt:10: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+    navController.navigate(route = TestClassWithArg::class)
+                                   ~~~~~~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:11: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+    navController.navigate(route = TestInterface)
+                                   ~~~~~~~~~~~~~
+src/com/example/test.kt:12: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+    navController.navigate(route = TestInterface::class)
+                                   ~~~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:13: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+    navController.navigate(route = InterfaceChildClass)
+                                   ~~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:14: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+    navController.navigate(route = InterfaceChildClass::class)
+                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:15: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+    navController.navigate(route = TestAbstract)
+                                   ~~~~~~~~~~~~
+src/com/example/test.kt:16: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+    navController.navigate(route = TestAbstract::class)
+                                   ~~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:17: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+    navController.navigate(route = AbstractChildClass)
+                                   ~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:18: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+    navController.navigate(route = AbstractChildClass::class)
+                                   ~~~~~~~~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:19: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+    navController.navigate(route = InterfaceChildClass::class)
+                                   ~~~~~~~~~~~~~~~~~~~~~~~~~~
+src/com/example/test.kt:20: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+    navController.navigate(route = Outer.InnerClass)
+                                   ~~~~~~~~~~~~~~~~
+src/com/example/test.kt:21: Error: The route should be a destination class instance or destination object. [WrongNavigateRouteType]
+    navController.navigate(route = Outer.InnerClass::class)
+                                   ~~~~~~~~~~~~~~~~~~~~~~~
+15 errors, 0 warnings
+                """
+            )
+    }
+
+    private val sourceCode =
+        """
+
+package androidx.navigation
+
+public open class NavController {
+
+    public fun navigate(resId: Int) {}
+
+    public fun navigate(route: String) {}
+
+    public fun <T : Any> navigate(route: T) {}
+}
+
+object TestObject
+
+class TestClass
+
+class TestClassWithArg(val arg: Int)
+
+val classInstanceRef = TestClass()
+
+val classInstanceWithArgRef = TestClassWithArg(15)
+
+object Outer {
+    data object InnerObject
+
+    data class InnerClass (
+        val innerArg: Int,
+    )
+}
+
+interface TestInterface
+class InterfaceChildClass(val arg: Boolean): TestInterface
+object InterfaceChildObject: TestInterface
+
+abstract class TestAbstract
+class AbstractChildClass(val arg: Boolean): TestAbstract()
+object AbstractChildObject: TestAbstract()
+"""
+
+    private val byteCode =
+        compiled(
+            "libs/StartDestinationLint.jar",
+            kotlin(sourceCode).indented(),
+            0xa3960069,
+            """
+                META-INF/main.kotlin_module:
+                H4sIAAAAAAAA/2NgYGBmYGBgBGJOBijgUuMSTsxLKcrPTKnQy0ssy0xPLMnM
+                zxPi90ssc87PKynKz8lJLfIu4RLl4k7Oz9VLrUjMLchJFWILSS0u8S5RYtBi
+                AADcysPxVwAAAA==
+                """,
+            """
+                androidx/navigation/AbstractChildClass.class:
+                H4sIAAAAAAAA/41QTW9SQRQ9M+8DeAV54Belamv9CGUhtHGnaaQkJiTYRW1Y
+                wGrgvdAJj/eSNwPpkt/i2o2JxsSFIS79UcY7j2pcsHAxZ+65c3LunfPz17fv
+                AF7iGcNzEQdpIoPrViyWciq0TOJWZ6x0Kia6eyWjoBsJpXJgDAfbtJeh0n/0
+                OVgM7msZS33KYDeGRwMGq3E0KMJBzoONPHGRThnYsAgPOwVwFEmqr6RiaPT/
+                b5tXNGUa6o4xIvshQ6U/S3Qk49a7UItAaEESPl9a9E1moGAANHZG/WtpWJuq
+                4JjhbL2qerzGs7Needzf8Xjeqq1XJ7zNzkpV1+d13rZ+fHC5b19U/rI8qet2
+                3vFd43TCcLh1/X8Doq1oCf9cLLtJrNMkisL0xUxTAN0kCBnKfRmH54v5OEwv
+                xTiiTrWfTEQ0EKk0/KbpvU8W6SR8Kw3ZvVjEWs7DgVSSXjtxnOhsssIxpWtn
+                /66asKniVDtwCQ+InRLndHvNryg0976g9CnTPCY0GuApDgnvbVS4hbKJkSrj
+                RqnDp7Pxapl06Xaan1H6uNWmuBHc2HA8yXCfXoE32ZIObo9g9XCnh7s9Gnuf
+                StR62EV9BKawhwcj5BTKCg8VPIVHCq6Cr1D5DV3dCrfVAgAA
+                """,
+            """
+                androidx/navigation/AbstractChildObject.class:
+                H4sIAAAAAAAA/41STW/TQBB9u0kcxw00lI8mFGhpQVAOuK24USGFCCRLwUg0
+                ioR6WturdhtnV7I3UY858UP4BxWHSiChCG78KMTYhA+JHrDlmX2zb99o3vrb
+                94+fATzBPYYHQieZUcmpr8VUHQmrjPa7UW4zEdvesUqT19GJjG0djGHjIvJA
+                5vbXgToqDM6+0so+Y6g83B42UYPjoYo6Q9Ueq5xhu/+fPZ8yuPtxWqp54IWE
+                G4QHg27Ye9HEJXgNKl5m2Oqb7Mg/kTbKhNK5L7Q2tlTN/dDYcJKmJHWlPzKW
+                xPxX0opEWEE1Pp5WyAlWhEYRwMBGVD9VBdqhVbJLDeYzz+NtXn7zmfv1HW/P
+                Z3t8hz2vu/zLe4e3eEHdY9i8cLi/PaK2rVBMe0bbzKSpzB6PLMPam4m2aiwD
+                PVW5ilLZ/TMEOdcziWRY7istw8k4ktlAEIdhpW9ikQ5Fpgq8KHoHZpLF8qUq
+                QGchPPxHFrtkX7WcuVO4SfkOIYdyizKnt1aidUJ+4Qzl2qNzuGfl9saCDNzH
+                XYrNnwQ0SApwsfT78Cqxi2fpE/jbczQ/YPmsLHBslvE2tsofkm6JBFYOUQlw
+                NcC1ANdxg5ZYDdBG5xAsx02s0X4OL8etHM4PGz/wlc0CAAA=
+                """,
+            """
+                androidx/navigation/InterfaceChildClass.class:
+                H4sIAAAAAAAA/41Qz28SQRT+ZhZY2FJZqFZK/dFatYWD0MabprElMSHBmtSG
+                A5wGdqVTltlkZyA98rd49mKiMfFgiEf/KOMbSpqYcPAw771v3jffe/P9/vPj
+                J4CX2GfYFypIYhlc15WYyqEwMlb1ljJh8lEMwualjIJmJLR2wRj8KzEV9Uio
+                Yf19/yocGBcOw+4qiYtQm1sZF2mGzGuppDlmSB10qx0G56DaycNFzkMKHmGR
+                DBlYN4881nPguENUcyk1Q7X9n1u+ojHD0JxYJdLvMhTbo9hEUtXfhUYEwgii
+                8PHUof8zG3I2gOaO6P5aWtSgKjhkOJ3PSh4v88WZzzzur3k865TnsyPeYKfr
+                pYzPK7zh/PqU4X7qvHiLssSupLJpP2OVjhj2Vu7/j0W0Fm3hn4lpM1YmiaMo
+                TF6MDFnQjIOQodCWKjybjPthciH6Ed2U2vFARB2RSIuXl96HeJIMwrfSgq3z
+                iTJyHHakltQ9USo2i9Eah+RvigZm6JSs4fRvTrWLLMUnhI4Jc8pe7TvWatvf
+                UPiy4OxRtK+AHTyluHnDgo+idZIqq0bGk+7GUqtuDaacrn1F4fNKmfwNYSnD
+                8WwRd/Gc8hvq3aXevR6cFjZbuN9CGVtUotLCNh70wDQe4lEPrkZR47FGXmNH
+                I6tR0tj4C6G2ycbxAgAA
+                """,
+            """
+                androidx/navigation/InterfaceChildObject.class:
+                H4sIAAAAAAAA/41SXWsTQRQ9M0mTzTbatH4l1q9aldoHty2+WYQaFBbiCjYE
+                pE+T3TGdZDMDu5PQxzz5Q/wHxYeCggR980eJd9ZYHxR0l7kfZ849M/fufvv+
+                8TOAx3jAsCV0khmVnARaTNVAWGV0EGors7cilu1jlSav+kMZ2yoYQ2MopiJI
+                hR4Ev9ASw8bfNLoyt+c6VSwxVPaVVvYpQ2nrYa+OKjwfZdQYyvZY5Qzbnf+9
+                yxMGbz9OCzkf3Gl4YXTYPYjaz+tYQb1GYINhs2OyQTCUtp8JpfNAaG1sIZsH
+                kbHRJE1JarUzMpbEgpfSikRYQRgfT0s0IuZMzRkwsBHhJ8plOxQlu3TAfOb7
+                vMmLNZ95X9/x5ny2x3fYs6rHv7yv8AZ31D13l39Oic5tRGLaNtpmJk1l9mhk
+                GdZfT7RVYxnqqcpVP5UHv7ug2bVNIhlWOkrLaDLuy6wriMOw1jGxSHsiUy5f
+                gP6hmWSxfKFc0loI9/6QxS7Nr0wtV2i13EDJ36G+Xb5GntNL34+yDcoCNxzy
+                S9tn8E+L7bsLMoiwSbb+k4BlikCFF86LrxHbPcufwN+c4eIHrJ4WAMe9wt7G
+                /eJnZbhEApePUApxJcTVkEqbFKIV4jrWj8By3MBN2s9Rz3Erh/cDKimwMukC
+                AAA=
+                """,
+            """
+                androidx/navigation/NavController.class:
+                H4sIAAAAAAAA/41SW08TQRT+Zttut8utrYJQQeWiFFC2EH2xhERJjEtqNdL0
+                hadpuynTbmeT3WnDY3+L/8AnjQ+G+OiPMp7ZNlBAo5vsuX/fnDNzfv769h3A
+                c+wzrHLZCgPROnckH4g2VyKQTpUPjgKpwsD3vTANxpDt8AF3fC7bzvtGx2uq
+                NBIM5oGQQh0yJIpb9WmkYNpIIs2QVGciYliv/JO9zGCNcx7hiu5WnSEVepHb
+                YmAuw3yxcnX2iQqFbJd1zXolCNtOx1ONkAsZOVzKQMUHRE41UNW+75c1U9BX
+                noUcw4NuoHwhnc6g5wipvFBy33GlZoxEM0rjDh3WPPOa3TH8Aw95z6NChs3J
+                JkYXUP5TW9OYx4KNu7jHkL9dcGOaMZGeZvmg9vJ25rBYq8Xp/O0cQ64ynuid
+                p3iLK04xozdI0NMyLTJagG6xS/Fzob0SWa09hvBiuGwbi4ZtZC+GtmFpg34r
+                Sdqif9ZaWLwY7hsl9jr145NJyeOVbKJglJJrlnUxzKa2KbVvZs2C8XZUkD6e
+                HRVQ1CKdmfCpqmTrk2nfdD812qdrS7DbVfT2R0GLVmCuIqRX7fcaXljjDd/T
+                wwdN7td5KLQ/Dm587Eslep4rByISFLp8rVdXi8CQORFtyVU/JIh9EvTDpvdG
+                aPzSGF8foSdAWIVBW6y/JHVLS01yhzxH9046tf0F1mcyDDwlacbBJJ6RnB4V
+                IAObdA5TcUSDX8T1NP5NoBkDF0bJMVBbM5glqSnmKKcpyqR1VXonn/+KxetE
+                JqwJovQlUZoolii/q23df3bcWAGJ/2G1/8q6THknrr5/nd1AKZbb2CNdoegK
+                XcmDUyRcPHTxyKUbXiMT6y428PgULMITbJ5iKoIdoRjBjLRNxlaEXIRChJnY
+                Lf4G70EG2roEAAA=
+                """,
+            """
+                androidx/navigation/NavControllerKt.class:
+                H4sIAAAAAAAA/41SXU8TQRQ9s1va7VLoFkRoAZEPsfWDBfVNYkKamGysNUGC
+                ITxN27EuLLPJzrThkd/iL1B5IJHEEB/9UcY7ayMBVNhs5s69c865987cHz+/
+                fgPwDKsMi1x2kjjsHPqS98Mu12Es/Sbv12OpkziKRPJK58AYvD3e537EZdd/
+                09oTbYraDONdoesRVyqQSnPZFpviPcN8tdb4m+6WUL/RzylxI066/p7QrYSH
+                UvlcylinMOU3Y93sRRGhvPYV8blrpAtwkM/DgstQuVzeu1B/2Ei6qVD1uioH
+                YCpjsv0vkeWbSRQwiqIpymNw1ttRKEP9gsGu1rYZZv8rkcMthux6yijgNsZd
+                TGCSYekmiXMoM2SqQW3bUKddVDDDUGrsx5pq8F8LzTtcc2rROujbNBTMLHmz
+                gIHtm41Fh4eh2dG8WJ01ho2zozH37Mi1pizXcuyBtRZK3tlRxVpl3z9mHTqv
+                ZBzLsymaoeDQeTDr5YzQE3reC5O2sq8Zpjd7UocHIpD9UIWtSGycDwa1Uo87
+                gqHYCKVo9g5aItnihGFw38a9pC1ehsYpDzS2ryhgjd4gY5qim6A5oSYfkpcl
+                myNbMc90KWajjCHyLDwib4ai5st8wfCn9JIeD7DA+AVeGQWMXGWVLrMmLrAc
+                jJEOS1lPkb4DJk8xsXOCqWMMn6Ky4xVPMHuM0uc/Qi6lMenNZFhYSdt7AJ9s
+                nRB3qPy5XdgB7gaYD7CAxQBLuBdgGfd3wRSqqO3CUebPKwwpZBVGFYoKBYWR
+                X85LYc0vBAAA
+                """,
+            """
+                androidx/navigation/Outer$InnerClass.class:
+                H4sIAAAAAAAA/41U31MbVRT+7m5+bJYAG6AtkNiqRExC2wVstRZaBRRZDKGC
+                w1jx5ZKsYWHZxd0NU18cnvondEZfnHEcn3ioMxocO+Ng++bf5Diem90mNVSG
+                meSec8+e853vnnPu/euf3/8AcAPrDHnu1DzXqj3QHX5g1XlguY6+2ghML284
+                jukt2Nz3k2AM2g4/4LrNnbq+urVjVoMkZIbErOVYwV2GWMEobjDIheJGGnEk
+                VcSgMCiWQJnz6gzMSENFTwoS0uQfbFs+w3j5PARmGHrqZmC0sSiNwaBW3b19
+                1zGdYIoAq+7+1wxF4nFezLGy69X1HTPY8rjl+Dp3HDdoeft6xQ0qDdueEYdJ
+                qMT5IkNapMjXzC95ww4YtgrnS2QY5e7azZyTYxqDGBLZR6mUgbseeJZDxx8q
+                FF+ADK10nkvdtvmGZddML4nLKq6Idgx1sAvPO3NHwWvUSL6/bzo1hmuF09Cn
+                s0XIRHAMeQH+BkNOlP4sxzeFY0E4LpztWBKOE2nk8IrQrtHht7m/veDWTIZM
+                J9JwArMuzjcZDiBNmI5pFVN4i05kftXgNs3YhcJL6v85zf5Z7afe8y3bpKrG
+                3WDb9BgGTqMQmfKuG9iWo6+YAa/xgJNN2juQ6X4xsaTEAhr+XbI/sMSOuEo1
+                GtgfTw4vq9KwpErayaFKP0lTVElJkOwhKZPsU54+VIZPDqelSTbfO5DQpFFp
+                Un76Q0LSYsspLSl2S88eysuDmkI6OSqKFDqRmZE5Rbo6rWg9o7FhNsmWnj2S
+                KTAdejxipPeS3if0tUwbXiE6ozElriUE12kmTjDyvwObxCLdxc5k0VtR4QcL
+                rhN4rm2b3vVduiyxsHn9ZcsxK429LdP7VNRXlNWtcnuDe5bYR8bsWsMJrD3T
+                cA4s3yLTXKc3DL3rAa/urvD9yDvf7X2Pe3zPJGr/CUt3KJq0Vdfdhlc1Fy0B
+                MRJBbJxKR8Mk0VsmSjAg3i/SFNLpVaB1mXaL9F0iqZaOkSplf0Xvz7ST8DGt
+                fRAdH6X4LFIky7S7GHrTt34xG6QJVBolaPQPMXUxMiTjpV/Qe9SGS7SM2RZM
+                OnSIYDJE7nnwWHcwe2kAvSwEKwKmiKXglHoC6X72GJcet4NCsqk22VREdiVi
+                cwHQUhjGSJR7PCpWJhf75lsogsFsKdtENoSs0CqDCQS621H62yQFtdwTXLl/
+                jFcHXm9iXEQ2UdSKTVxt4vrjrmPkIkYv8KBVb9dgPKpBi8FvuNFdBiWKZ7iJ
+                tyMeX5AU7cqXJn5CPHY08Sek7xCXjyZOIK0IoKv0/15YYmFPKq32yUnlb2SS
+                tO9ULN+uWB638C7lWSU9KUi906rBvVYoXS98hCUq3yctQANrJD8j+23q1Mwm
+                ZAOzBu4YuIv3SMX7BuYwvwnmYwEfbKLfF78PfaitNeFD85HxMeBj0MfNlvGW
+                D91HjvR/AXBODlL6BwAA
+                """,
+            """
+                androidx/navigation/Outer$InnerObject.class:
+                H4sIAAAAAAAA/41US08UQRD+umcfs7M8loc8FR8s8lJmQTxBTJBoHLIsxiUY
+                5dTsjjAwzOhM74YjJ29ePXj04ImDxAOJJgYlXvxRxOrZISAEY7Lb/VV1VX3V
+                X/Xu7+Ov3wFM4z7DkPCqge9Ud0xP1J11IR3fM5dq0g7ylufZwdLapl2RaTCG
+                3KaoC9MV3rp54tUYUrOO58gHDNrI6EoTkkgZSCDNkJAbTsgwXPwvhhkGXfpl
+                GTjeOkPnyGjxlK3hpYjBoh+sm5u2XAuE44Wm8DxfRgVDs+TLUs11KSp7pqyO
+                Fiq8IcKNeb9qR01a2q/p43fUuP2mJlzq8MpI8fzNZkZfMuT/xUZUYs21iS7p
+                yw07YGi/WIWoZytupI8BrkTRrVJ5ea40/6gJfTAy5OxnaCtu+ZLCzEVbiqqQ
+                ghL5dl2jGTG1ZNQCBrZF/h1HWQVC1UmGV4e7Awbv4QbPHe4aXFcgG++6oVy5
+                Fv3ordFzuDvFC+xhWuc/P6Z4ji905LQ+XkhM6blkX6KHFdiTo/faQiaXIm+a
+                MCOsE84orNimmOqh99JppjFKb6Qk6vO+JwPfde1gYksy9D+redLZti2v7oQO
+                aTZ3qiO9ksZcWouOZ5dq22t2sKx0VXL6FeGuiMBRduxsLktR2VoUr2M7f772
+                UxGIbZu6+YukKXoR864IQ5tMo+zXgor92FEleuMSKxeawySNJxEp36umRfsd
+                slK0N9OepNNkZN0ly1TzUd6xA+j7BDgm4mCgh46BpkYAMlRKFc2Sh0fJN+Nk
+                rb31c3R0Gq7F4WeZSWa0xbynqe17l6QydKAzZrJo57R3j41/QjKxN/4D/AOS
+                2t74IfjzxF7UeIHWBHhaj4p1NRLiYgp10ZeROlAvmn4/BHS644kU3VECkP0G
+                /uIAvV9wdT9yaJiiVenIMYYWUvVexDdOf0WqNYZrJM/AKjQL1y3csOh2twhi
+                0EIeQ6tgIW5jeBVGqD4jIVIhOiLQFSIXgSytfwCP7WED4AQAAA==
+                """,
+            """
+                androidx/navigation/Outer.class:
+                H4sIAAAAAAAA/4VRW2sTQRg9M5vLZhNtGi9JrK2XptpUcNviUy1CDQoLMYW2
+                BCRPk2SIk2xmYXcS+pgnf4j/oPhQUJCgb/4o8dttNA9S3GG/M9/tfDNnfv76
+                8g3ACzxjqArdDwPVP3e1mKqBMCrQ7vHEyDALxlAciqlwfaEH7nF3KHsmC4sh
+                c6i0Mq8YrO16u4A0Mg5SyDKkzAcVMaw1r2V9yWAf9vyk3wGPm2yvdXp21Gq8
+                KeAGnBwFbzJsNoNw4A6l6YZC6cgVWgcm4YncVmBaE98nqtXmKDBE5r6TRvSF
+                ERTj46lFt2OxycUGDGxE8XMVe7u06+8x1OezgsMr3OHF+czhtmX/+Mgr89k+
+                32UH3Eq9ztr8+6cML/K4YZ/FNI6ntQwbvojokvnEuVKFoXbtjWvLpiweMGz9
+                p/KPzo9I/ZaYNgJtwsD3Zfh8RHPWTibaqLH09FRFquvLo6UwpH8j6EuGlabS
+                sjUZd2V4JqiGodQMesJvi1DF/iJYWJ5MUrNzGkzCnnyr4lx1Maf9zxTs0Qul
+                Elmr8YMR1sjLEBYJOa104m2R58biE6Z3LmFfJOkni2KgjKdkC1cFyBEVYCP/
+                t7lM1fGX/wr+/hKFz1i5SAIWtsmWKP2Q/nU6x2PCDcJ6MmITO4QHRLNKxKUO
+                LA+3PNz2cAd3aYuyhwqqHbAI97DWQTqCE+F+hEyE9QgbvwFBj+0jIgMAAA==
+                """,
+            """
+                androidx/navigation/TestAbstract.class:
+                H4sIAAAAAAAA/4VRy0oDMRQ9SduxjlWnPusLfICoC0fFnSKoIBSqgko3rtJO
+                0NhpApO0uOy3+AeuBBdSXPpR4s3o3s3hPG7Cyc3X9/sHgEOsMKwKnWRGJc+x
+                Fn31IJwyOr6T1p22rMtE242AMURPoi/iVOiH+Lr1JL1bYAiOlVbuhKGwtd2s
+                oIQgRBEjDEX3qCzDeuO/y48Yqo2OcanS8aV0IhFOkMe7/QIVZB5GPYCBdch/
+                Vl7tEUv2qftwEIa8xkMeERsOypu14eCA77Gz0udLwCPu5w6YPx1dif650S4z
+                aSqz3Y6jkucmkQyTDaXlVa/bktmdaKXkTDVMW6RNkSmv/8zw1vSytrxQXizc
+                9LRTXdlUVlF6qrVx+eNscQ2cdvBX2a+EsEYqzjVQ2nlD+ZUIxwJhkJsbWCSs
+                /A5gFGGeL+U4j+X8rxjGKKvco1DHeB0TdUwiIopqHVOYvgezmMEs5RahxZxF
+                8AMTgVUI6AEAAA==
+                """,
+            """
+                androidx/navigation/TestClass.class:
+                H4sIAAAAAAAA/31RO0sDQRD+ZmMuekY93/HdqoWnYqcIGhACUUEljdUmt+ia
+                yy7cboJlfov/wEqwkGDpjxLnTmubj+8xM8zsfn2/fwA4wgZhQ5okszp5jo0c
+                6AfptTXxnXK+nkrnKiBC9CQHMk6leYiv20+q4ysoEYITbbQ/JZS2d1pVlBGE
+                GEOFMOYftSNsNf+dfEyYbXatT7WJL5WXifSSPdEblHg1ymEiBxCoy/6zztU+
+                s+SAsDkahqGoiVBEzEbD2mh4KPbpvPz5EohI5FWHlPdGV3JQt8ZnNk1Vttf1
+                vF/dJoow09RGXfV7bZXdyXbKzlzTdmTakpnO9Z8Z3tp+1lEXOhcrN33jdU+1
+                tNOcnhljfXGXwwEEn/+3cP4ajDVWcaGB8u4bxl+ZCKwwBoU5g1XG6m8BJhAW
+                +VqBy1gv/ogwyVn1HqUGphqYbnBXxBSzDcxh/h7ksIBFzh1ChyWH4AcFuY/X
+                4AEAAA==
+                """,
+            """
+                androidx/navigation/TestClassWithArg.class:
+                H4sIAAAAAAAA/41QTYvTUBQ97yVN09jatH51On47yEwXpjO4UwZrQQjUEcah
+                Lrp6bUPnTdMXyHsts+xvce1GUAQXUlz6o8T70sGVCyE5956bw7m559fv7z8A
+                PMcew55Q0zyT08tIiZWcCSMzFZ0l2vRTofUHac57+awMxhBeiJWIUqFm0bvx
+                RTIxZTgM3kuppDlmcPfjgyGDs38wrKKEcgAXPnGRzxhYXEWAaxVwVElqzqVm
+                eDr4n90vaMcsMT1rQ+YxQ2Mwz0wqVfQ2MWIqjCAJX6wcOolZqFgALZ3T/FJa
+                1qVuesjQ36ybAW/xgIebdUAPD/2A+05rsz7iXfa61vRC3uZd5+dHj4fuaeMv
+                80nddv1S6FmrI2YXhCdi1c+UybM0TfJnc0On9bNpwlAfSJWcLBfjJD8T45Qm
+                zUE2EelQ5NLyq2HwPlvmk+SNtGTndKmMXCRDqSV97SmVmSISjUPKzS1uatoY
+                qePUl+ARPiB2TJxTDTrfUOnsfkXtc6F5SGg1QAOPCG9vVbiOuo2IOutGiSKk
+                d+sV2eSoljpfUPv0T5vqVnBlw/G4wPt4QvVV8ZMl3BjBiXEzxq2Y1t6hFq0Y
+                O2iPwDR2cXeEskZd455GUKCnEWo0/gB/P3a9nQIAAA==
+                """,
+            """
+                androidx/navigation/TestInterface.class:
+                H4sIAAAAAAAA/4WOz0rDQBDGv9lo08Z/qVqoR/Fu2tKbJykIgaqg4iWnbbIt
+                22x3IbsNPfa5PEjPPpR0Ux/AGfjmmxn4zfz8fn0DGKNHuOW6qIwsNonmtVxw
+                J41OPoR1qXaimvNchCBCvOQ1TxTXi+R1thS5CxEQutPSOCV18iwcL7jjDwS2
+                qgMPp0Y6jYBApZ9vZNMNvCuGhN5u245Yn0Us9m7e321HbEDNckS4m/77lb/k
+                wfELrydGu8ooJar70hGid7OucvEklSDcvK21kyvxKa2cKfGotXEHmG35UzjC
+                XzBcHfQS174OPfjYZytDkCJM0U7RQeQtTlKc4iwDWZzjIgOziC26e5qGvyhR
+                AQAA
+                """,
+            """
+                androidx/navigation/TestObject.class:
+                H4sIAAAAAAAA/31STW/TQBB9u0kcxw00lI8mFEqhPQAH3FbcqJBKBJKlYCQa
+                Rap62sSrsImzK9kbq8ec+CH8g4pDJZBQBDd+FGLWBDggYUsz896+edoZ+/uP
+                T18APMUew7bQSWZUch5qUaixsMrosC9z+2Y4kSNbB2NoTUQhwlTocfibrTB4
+                R0or+5yh8vDRoIkavABV1Bmq9p3KGXZ6/7d+xuAfjdLSJAB3nX4Un/SP4+7L
+                Jq4gaBB5lWG3Z7JxOJF2mAml81BobWxploexsfE8TcnqWm9qLJmFr6UVibCC
+                OD4rKjQnc6HhAhjYlPhz5dA+VckBw95yEQS8zQPeomq58L+95+3l4pDvsxd1
+                n3/94PEWd9pD5hxasSi6RtvMpKnMnkwtw9bbubZqJiNdqFwNU3n89460j65J
+                JMN6T2kZz2dDmfUFaRg2emYk0oHIlMMrMjgx82wkXykHOivjwT+2OKDtVMuR
+                Om5ZlLcJee6ClDm9tRLdIxS6wSnXHl/CvyiPd1ZiUPN9is1fAjTICvCx9qd5
+                k9TuWfsMfnqJ5kesX5QEx4My3sVu+TfRRyCDjTNUIlyPcCPCTdyiEpsR2uic
+                geW4jS06zxHkuJPD+wnyhROwigIAAA==
+                """
+        )
+}
diff --git a/pdf/pdf-viewer/src/main/res/values-sq/strings.xml b/pdf/pdf-viewer/src/main/res/values-sq/strings.xml
index edaa770..bbc74df 100644
--- a/pdf/pdf-viewer/src/main/res/values-sq/strings.xml
+++ b/pdf/pdf-viewer/src/main/res/values-sq/strings.xml
@@ -46,16 +46,10 @@
     <string name="label_page_range" msgid="8290964180158460076">"<xliff:g id="FIRST">%1$d</xliff:g>-<xliff:g id="LAST">%2$d</xliff:g> / <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
     <string name="desc_page_range" msgid="5286496438609641577">"faqet nga <xliff:g id="FIRST">%1$d</xliff:g> deri në <xliff:g id="LAST">%2$d</xliff:g> nga <xliff:g id="TOTAL">%3$d</xliff:g>"</string>
     <string name="desc_image_alt_text" msgid="7700601988820586333">"Imazhi: <xliff:g id="ALT_TEXT">%1$s</xliff:g>"</string>
-    <!-- no translation found for hint_find (5385388836603550565) -->
-    <skip />
-    <!-- no translation found for message_no_matches_found (6965828658999779258) -->
-    <skip />
-    <!-- no translation found for message_match_status (6288242289981639727) -->
-    <skip />
-    <!-- no translation found for action_edit (5882082700509010966) -->
-    <skip />
-    <!-- no translation found for password_not_entered (8875370870743585303) -->
-    <skip />
-    <!-- no translation found for retry_button_text (3443862378337999137) -->
-    <skip />
+    <string name="hint_find" msgid="5385388836603550565">"Gjej te skedari"</string>
+    <string name="message_no_matches_found" msgid="6965828658999779258">"Nuk u gjetën përputhje."</string>
+    <string name="message_match_status" msgid="6288242289981639727">"<xliff:g id="POSITION">%1$d</xliff:g> / <xliff:g id="TOTAL">%2$d</xliff:g>"</string>
+    <string name="action_edit" msgid="5882082700509010966">"Modifiko skedarin"</string>
+    <string name="password_not_entered" msgid="8875370870743585303">"Fut fjalëkalimin për ta shkyçur"</string>
+    <string name="retry_button_text" msgid="3443862378337999137">"Riprovo"</string>
 </resources>
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyUiInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyUiInterfaceClientProxy.kt
index 4d8eeb9..c06a863 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyUiInterfaceClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyUiInterfaceClientProxy.kt
@@ -6,6 +6,7 @@
 import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient
+import androidx.privacysandbox.ui.core.SessionObserverFactory
 import java.util.concurrent.Executor
 
 public class MyUiInterfaceClientProxy(
@@ -31,4 +32,12 @@
         sandboxedUiAdapter.openSession(context, windowInputToken, initialWidth, initialHeight,
                 isZOrderOnTop, clientExecutor, client)
     }
+
+    public override fun addObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+        sandboxedUiAdapter.addObserverFactory(sessionObserverFactory)
+    }
+
+    public override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+        sandboxedUiAdapter.removeObserverFactory(sessionObserverFactory)
+    }
 }
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt
index d7f90e3..2a26042 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt
@@ -6,6 +6,7 @@
 import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient
+import androidx.privacysandbox.ui.core.SessionObserverFactory
 import java.util.concurrent.Executor
 
 public class MySecondInterfaceClientProxy(
@@ -31,4 +32,12 @@
         sandboxedUiAdapter.openSession(context, windowInputToken, initialWidth, initialHeight,
                 isZOrderOnTop, clientExecutor, client)
     }
+
+    public override fun addObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+        sandboxedUiAdapter.addObserverFactory(sessionObserverFactory)
+    }
+
+    public override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+        sandboxedUiAdapter.removeObserverFactory(sessionObserverFactory)
+    }
 }
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyUiInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyUiInterfaceClientProxy.kt
index 86709a8..b16b160 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyUiInterfaceClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyUiInterfaceClientProxy.kt
@@ -6,6 +6,7 @@
 import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient
+import androidx.privacysandbox.ui.core.SessionObserverFactory
 import java.util.concurrent.Executor
 
 public class MyUiInterfaceClientProxy(
@@ -31,4 +32,12 @@
         sandboxedUiAdapter.openSession(context, windowInputToken, initialWidth, initialHeight,
                 isZOrderOnTop, clientExecutor, client)
     }
+
+    public override fun addObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+        sandboxedUiAdapter.addObserverFactory(sessionObserverFactory)
+    }
+
+    public override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+        sandboxedUiAdapter.removeObserverFactory(sessionObserverFactory)
+    }
 }
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
index 54d5210..78c6c592 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
@@ -100,6 +100,8 @@
                             .build()
                     )
                     addFunction(generateOpenSession())
+                    addFunction(generateAddObserverFactory())
+                    addFunction(generateRemoveObserverFactory())
                 }
             }
 
@@ -171,6 +173,38 @@
             )
         }
 
+    private fun generateAddObserverFactory() =
+        FunSpec.builder("addObserverFactory").build {
+            addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE)
+            addParameters(
+                listOf(
+                    ParameterSpec(
+                        "sessionObserverFactory",
+                        ClassName("androidx.privacysandbox.ui.core", "SessionObserverFactory")
+                    )
+                )
+            )
+            addStatement(
+                "$sandboxedUiAdapterPropertyName.addObserverFactory(" + "sessionObserverFactory)"
+            )
+        }
+
+    private fun generateRemoveObserverFactory() =
+        FunSpec.builder("removeObserverFactory").build {
+            addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE)
+            addParameters(
+                listOf(
+                    ParameterSpec(
+                        "sessionObserverFactory",
+                        ClassName("androidx.privacysandbox.ui.core", "SessionObserverFactory")
+                    )
+                )
+            )
+            addStatement(
+                "$sandboxedUiAdapterPropertyName.removeObserverFactory(" + "sessionObserverFactory)"
+            )
+        }
+
     private fun generateTransactionCallbackObject(method: Method) =
         CodeBlock.builder().build {
             val transactionCallbackClassName =
diff --git a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt
index 3a3f304..71d47a9 100644
--- a/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt
+++ b/privacysandbox/ui/integration-tests/sdkproviderutils/src/main/java/androidx/privacysandbox/ui/integration/sdkproviderutils/TestAdapters.kt
@@ -77,7 +77,7 @@
                 )
         }
 
-        private inner class BannerAdSession(private val adView: View) : SandboxedUiAdapter.Session {
+        private inner class BannerAdSession(private val adView: View) : AbstractSession() {
             override val view: View
                 get() = adView
 
diff --git a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
index b5f2651..3e536c5 100644
--- a/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
+++ b/privacysandbox/ui/integration-tests/testsdkprovider/src/main/java/androidx/privacysandbox/ui/integration/testsdkprovider/SdkApi.kt
@@ -19,10 +19,16 @@
 import android.content.Context
 import android.os.Bundle
 import android.os.Process
+import android.util.Log
 import android.view.View
 import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
 import androidx.privacysandbox.ui.client.SandboxedUiAdapterFactory
 import androidx.privacysandbox.ui.client.view.SandboxedSdkView
+import androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SessionObserver
+import androidx.privacysandbox.ui.core.SessionObserverContext
+import androidx.privacysandbox.ui.core.SessionObserverFactory
 import androidx.privacysandbox.ui.integration.sdkproviderutils.SdkApiConstants.Companion.AdType
 import androidx.privacysandbox.ui.integration.sdkproviderutils.SdkApiConstants.Companion.MediationOption
 import androidx.privacysandbox.ui.integration.sdkproviderutils.TestAdapters
@@ -33,6 +39,7 @@
 
 class SdkApi(private val sdkContext: Context) : ISdkApi.Stub() {
     private val testAdapters = TestAdapters(sdkContext)
+    private val measurementManager = MeasurementManager()
 
     override fun loadBannerAd(
         @AdType adType: Int,
@@ -73,7 +80,9 @@
     }
 
     private fun loadWebViewBannerAdFromLocalAssets(): Bundle {
-        return testAdapters.WebViewAdFromLocalAssets().toCoreLibInfo(sdkContext)
+        val ad = testAdapters.WebViewAdFromLocalAssets()
+        measurementManager.startObserving(ad)
+        return ad.toCoreLibInfo(sdkContext)
     }
 
     private fun loadNonWebViewBannerAd(text: String, waitInsideOnDraw: Boolean): Bundle {
@@ -139,8 +148,39 @@
         return null
     }
 
+    class MeasurementManager {
+        fun startObserving(adapter: SandboxedUiAdapter) {
+            adapter.addObserverFactory(SessionObserverFactoryImpl())
+        }
+
+        private inner class SessionObserverFactoryImpl : SessionObserverFactory {
+
+            override fun create(): SessionObserver {
+                return SessionObserverImpl()
+            }
+
+            private inner class SessionObserverImpl : SessionObserver {
+
+                override fun onSessionOpened(sessionObserverContext: SessionObserverContext) {
+                    Log.i(TAG, "onSessionOpened $sessionObserverContext")
+                }
+
+                override fun onUiContainerChanged(uiContainerInfo: Bundle) {
+                    // TODO(b/330515740): Reflect this event in the app UI.
+                    val sandboxedSdkViewUiInfo = SandboxedSdkViewUiInfo.fromBundle(uiContainerInfo)
+                    Log.i(TAG, "onUiContainerChanged $sandboxedSdkViewUiInfo")
+                }
+
+                override fun onSessionClosed() {
+                    Log.i(TAG, "session closed")
+                }
+            }
+        }
+    }
+
     companion object {
         private const val MEDIATEE_SDK =
             "androidx.privacysandbox.ui.integration.mediateesdkprovider"
+        private const val TAG = "SdkApi"
     }
 }
diff --git a/privacysandbox/ui/ui-client/build.gradle b/privacysandbox/ui/ui-client/build.gradle
index fbf7583..da5ff2f 100644
--- a/privacysandbox/ui/ui-client/build.gradle
+++ b/privacysandbox/ui/ui-client/build.gradle
@@ -54,6 +54,7 @@
     androidTestImplementation(libs.multidex)
     androidTestImplementation(libs.testUiautomator)
     androidTestImplementation project(path: ':appcompat:appcompat')
+    androidTestImplementation project(":privacysandbox:ui:ui-provider")
 }
 
 android {
diff --git a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
index eec0931..7eb9e7a 100644
--- a/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
+++ b/privacysandbox/ui/ui-client/src/androidTest/java/androidx/privacysandbox/ui/client/test/SandboxedSdkViewTest.kt
@@ -21,7 +21,9 @@
 import android.content.pm.ActivityInfo
 import android.content.res.Configuration
 import android.graphics.Rect
+import android.os.Bundle
 import android.os.IBinder
+import android.os.SystemClock
 import android.view.SurfaceView
 import android.view.View
 import android.view.ViewGroup
@@ -33,7 +35,9 @@
 import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionStateChangedListener
 import androidx.privacysandbox.ui.client.view.SandboxedSdkView
 import androidx.privacysandbox.ui.core.BackwardCompatUtil
+import androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.provider.AbstractSandboxedUiAdapter
 import androidx.test.espresso.Espresso.onView
 import androidx.test.espresso.assertion.ViewAssertions.matches
 import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
@@ -46,9 +50,11 @@
 import androidx.test.uiautomator.Until
 import androidx.testutils.withActivity
 import com.google.common.truth.Truth.assertThat
+import java.lang.Long.min
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executor
 import java.util.concurrent.TimeUnit
+import kotlin.Long.Companion.MAX_VALUE
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Assume.assumeTrue
@@ -64,8 +70,11 @@
 
     companion object {
         const val TIMEOUT = 1000.toLong()
+
         // Longer timeout used for expensive operations like device rotation.
         const val UI_INTENSIVE_TIMEOUT = 2000.toLong()
+
+        const val SHORTEST_TIME_BETWEEN_SIGNALS_MS = 200
     }
 
     private lateinit var context: Context
@@ -78,7 +87,7 @@
 
     @get:Rule var activityScenarioRule = ActivityScenarioRule(UiLibActivity::class.java)
 
-    class FailingTestSandboxedUiAdapter : SandboxedUiAdapter {
+    class FailingTestSandboxedUiAdapter : AbstractSandboxedUiAdapter() {
         override fun openSession(
             context: Context,
             windowInputToken: IBinder,
@@ -92,7 +101,8 @@
         }
     }
 
-    class TestSandboxedUiAdapter : SandboxedUiAdapter {
+    class TestSandboxedUiAdapter(private val signalOptions: Set<String> = setOf("option")) :
+        AbstractSandboxedUiAdapter() {
 
         var isSessionOpened = false
         var internalClient: SandboxedUiAdapter.SessionClient? = null
@@ -118,7 +128,7 @@
             client: SandboxedUiAdapter.SessionClient
         ) {
             internalClient = client
-            testSession = TestSession(context, initialWidth, initialHeight)
+            testSession = TestSession(context, initialWidth, initialHeight, signalOptions)
             if (!delayOpenSessionCallback) {
                 client.onSessionOpened(testSession!!)
             }
@@ -148,9 +158,15 @@
             context: Context,
             initialWidth: Int,
             initialHeight: Int,
+            override val signalOptions: Set<String>
         ) : SandboxedUiAdapter.Session {
 
             var zOrderChangedLatch: CountDownLatch = CountDownLatch(1)
+            var shortestGapBetweenUiChangeEvents = MAX_VALUE
+            private var notifyUiChangedLatch: CountDownLatch = CountDownLatch(1)
+            private var latestUiChange: Bundle = Bundle()
+            private var hasReceivedFirstUiChange = false
+            private var timeReceivedLastUiChange = SystemClock.elapsedRealtime()
 
             override val view: View = View(context)
 
@@ -176,6 +192,37 @@
             }
 
             override fun close() {}
+
+            override fun notifyUiChanged(uiContainerInfo: Bundle) {
+                if (hasReceivedFirstUiChange) {
+                    shortestGapBetweenUiChangeEvents =
+                        min(
+                            shortestGapBetweenUiChangeEvents,
+                            SystemClock.elapsedRealtime() - timeReceivedLastUiChange
+                        )
+                }
+                hasReceivedFirstUiChange = true
+                timeReceivedLastUiChange = SystemClock.elapsedRealtime()
+                latestUiChange = uiContainerInfo
+                notifyUiChangedLatch.countDown()
+            }
+
+            fun assertNoSubsequentUiChanges() {
+                notifyUiChangedLatch = CountDownLatch(1)
+                assertThat(notifyUiChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+            }
+
+            /**
+             * Performs the action specified in the Runnable, and waits for the next UI change.
+             *
+             * Throws an [AssertionError] if no UI change is reported.
+             */
+            fun runAndRetrieveNextUiChange(runnable: Runnable): SandboxedSdkViewUiInfo {
+                notifyUiChangedLatch = CountDownLatch(1)
+                runnable.run()
+                assertThat(notifyUiChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+                return SandboxedSdkViewUiInfo.fromBundle(latestUiChange)
+            }
         }
     }
 
@@ -590,12 +637,156 @@
         )
     }
 
-    private fun addViewToLayout(waitToBeActive: Boolean = false) {
+    @Test
+    fun signalsOnlyCollectedWhenSignalOptionsNonEmpty() {
+        addViewToLayoutAndWaitToBeActive()
+        assertThat(view.signalMeasurer).isNotNull()
+        val adapter = TestSandboxedUiAdapter(setOf())
+        val view2 = SandboxedSdkView(context)
+        activityScenarioRule.withActivity { view2.setAdapter(adapter) }
+        addViewToLayoutAndWaitToBeActive(view2)
+        assertThat(view2.signalMeasurer).isNull()
+    }
+
+    @Test
+    fun signalsNotSentWhenViewUnchanged() {
+        addViewToLayoutAndWaitToBeActive()
+        val session = testSandboxedUiAdapter.testSession!!
+        session.runAndRetrieveNextUiChange {}
+        session.assertNoSubsequentUiChanges()
+    }
+
+    @Test
+    fun signalsSentWhenSizeChanges() {
+        addViewToLayoutAndWaitToBeActive()
+        val session = testSandboxedUiAdapter.testSession!!
+        val newWidth = 500
+        val newHeight = 500
+        val sandboxedSdkViewUiInfo =
+            session.runAndRetrieveNextUiChange {
+                activityScenarioRule.withActivity {
+                    view.layoutParams = LinearLayout.LayoutParams(newWidth, newHeight)
+                }
+            }
+        assertThat(sandboxedSdkViewUiInfo.uiContainerWidth).isEqualTo(newWidth)
+        assertThat(sandboxedSdkViewUiInfo.uiContainerHeight).isEqualTo(newHeight)
+        assertThat(session.shortestGapBetweenUiChangeEvents)
+            .isAtLeast(SHORTEST_TIME_BETWEEN_SIGNALS_MS)
+    }
+
+    /**
+     * Shifts the view partially off screen and verifies that the reported onScreenGeometry is
+     * cropped accordingly.
+     */
+    @Test
+    fun correctSignalsSentForOnScreenGeometryWhenViewOffScreen() {
+        val clippedWidth = 400
+        val clippedHeight = 500
+        activityScenarioRule.withActivity {
+            val layoutParams = findViewById<LinearLayout>(R.id.mainlayout).layoutParams
+            layoutParams.width = clippedWidth
+            layoutParams.height = clippedHeight
+        }
+        addViewToLayoutAndWaitToBeActive()
+        val session = testSandboxedUiAdapter.testSession!!
+        val initialHeight = view.height
+        val initialWidth = view.width
+        val xShiftDistance = 200f
+        val yShiftDistance = 300f
+        val sandboxedSdkViewUiInfo =
+            session.runAndRetrieveNextUiChange {
+                activityScenarioRule.withActivity {
+                    view.y -= yShiftDistance
+                    view.x -= xShiftDistance
+                }
+            }
+        assertThat(sandboxedSdkViewUiInfo.uiContainerWidth).isEqualTo(clippedWidth)
+        assertThat(sandboxedSdkViewUiInfo.uiContainerHeight).isEqualTo(clippedHeight)
+        assertThat(sandboxedSdkViewUiInfo.onScreenGeometry.height().toFloat())
+            .isEqualTo(initialHeight - yShiftDistance)
+        assertThat(sandboxedSdkViewUiInfo.onScreenGeometry.width().toFloat())
+            .isEqualTo(initialWidth - xShiftDistance)
+    }
+
+    @Test
+    fun signalsSentWhenPositionChanges() {
+        addViewToLayoutAndWaitToBeActive()
+        val session = testSandboxedUiAdapter.testSession!!
+        val newXPosition = 100f
+        val sandboxedSdkViewUiInfo =
+            session.runAndRetrieveNextUiChange {
+                activityScenarioRule.withActivity { view.x = newXPosition }
+            }
+        val containerWidth = sandboxedSdkViewUiInfo.uiContainerWidth
+        val onScreenWidth = sandboxedSdkViewUiInfo.onScreenGeometry.width().toFloat()
+        assertThat(containerWidth - newXPosition).isEqualTo(onScreenWidth)
+    }
+
+    @Test
+    fun signalsSentWhenAlphaChanges() {
+        addViewToLayoutAndWaitToBeActive()
+        val session = testSandboxedUiAdapter.testSession!!
+        // Catch initial UI change so that the subsequent alpha change will be reflected in the
+        // next SandboxedSdkViewUiInfo
+        session.runAndRetrieveNextUiChange {}
+        val newAlpha = 0.5f
+        val sandboxedSdkViewUiInfo =
+            session.runAndRetrieveNextUiChange {
+                activityScenarioRule.withActivity { view.alpha = newAlpha }
+            }
+        assertThat(sandboxedSdkViewUiInfo.uiContainerOpacityHint).isEqualTo(newAlpha)
+    }
+
+    /**
+     * Changes the size of the view several times in quick succession, and verifies that the signals
+     * sent match the width of the final change.
+     */
+    @Test
+    fun signalsSentAreFresh() {
+        addViewToLayoutAndWaitToBeActive()
+        val session = testSandboxedUiAdapter.testSession!!
+        var currentWidth = view.width
+        var currentHeight = view.height
+        val sandboxedSdkViewUiInfo =
+            session.runAndRetrieveNextUiChange {
+                activityScenarioRule.withActivity {
+                    for (i in 1..5) {
+                        view.layoutParams =
+                            LinearLayout.LayoutParams(currentWidth + 10, currentHeight + 10)
+                        currentWidth += 10
+                        currentHeight += 10
+                    }
+                }
+            }
+        assertThat(sandboxedSdkViewUiInfo.uiContainerWidth).isEqualTo(currentWidth)
+        assertThat(sandboxedSdkViewUiInfo.uiContainerHeight).isEqualTo(currentHeight)
+    }
+
+    /**
+     * Creates many UI changes and ensures that these changes are not sent more frequently than
+     * expected.
+     */
+    @Test
+    @SuppressLint("BanThreadSleep") // Deliberate delay for testing
+    fun signalsNotSentMoreFrequentlyThanLimit() {
+        addViewToLayoutAndWaitToBeActive()
+        val session = testSandboxedUiAdapter.testSession!!
+        for (i in 1..10) {
+            activityScenarioRule.withActivity {
+                view.layoutParams = LinearLayout.LayoutParams(view.width + 10, view.height + 10)
+            }
+            Thread.sleep(100)
+        }
+        assertThat(session.shortestGapBetweenUiChangeEvents)
+            .isAtLeast(SHORTEST_TIME_BETWEEN_SIGNALS_MS)
+    }
+
+    private fun addViewToLayout(waitToBeActive: Boolean = false, viewToAdd: View = view) {
         activityScenarioRule.withActivity {
             val mainLayout: LinearLayout = findViewById(R.id.mainlayout)
             mainLayoutWidth = mainLayout.width
             mainLayoutHeight = mainLayout.height
-            mainLayout.addView(view)
+            mainLayout.addView(viewToAdd)
         }
         if (waitToBeActive) {
             val latch = CountDownLatch(1)
@@ -608,8 +799,8 @@
         }
     }
 
-    private fun addViewToLayoutAndWaitToBeActive() {
-        addViewToLayout(true)
+    private fun addViewToLayoutAndWaitToBeActive(viewToAdd: View = view) {
+        addViewToLayout(true, viewToAdd)
     }
 
     private fun requestSizeAndVerifyLayout(
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
index 0eb7423..7504162 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/SandboxedUiAdapterFactory.kt
@@ -35,6 +35,7 @@
 import androidx.privacysandbox.ui.core.IRemoteSessionController
 import androidx.privacysandbox.ui.core.ISandboxedUiAdapter
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SessionObserverFactory
 import java.lang.reflect.InvocationHandler
 import java.lang.reflect.Method
 import java.lang.reflect.Proxy
@@ -81,6 +82,7 @@
      * [LocalAdapter] fetches UI from a provider living on same process as the client but on a
      * different class loader.
      */
+    @SuppressLint("BanUncheckedReflection") // using reflection on library classes
     private class LocalAdapter(adapterInterface: ISandboxedUiAdapter) : SandboxedUiAdapter {
         private val uiProviderBinder = adapterInterface.asBinder()
 
@@ -111,7 +113,6 @@
                     targetSessionClientClass
                 )
 
-        @SuppressLint("BanUncheckedReflection") // using reflection on library classes
         override fun openSession(
             context: Context,
             windowInputToken: IBinder,
@@ -145,11 +146,14 @@
             }
         }
 
+        override fun addObserverFactory(sessionObserverFactory: SessionObserverFactory) {}
+
+        override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) {}
+
         private class SessionClientProxyHandler(
             private val origClient: SandboxedUiAdapter.SessionClient,
         ) : InvocationHandler {
 
-            @SuppressLint("BanUncheckedReflection") // using reflection on library classes
             override fun invoke(proxy: Any, method: Method, args: Array<Any>?): Any {
                 return when (method.name) {
                     "onSessionOpened" -> {
@@ -198,33 +202,39 @@
             private val getViewMethod = targetClass.getMethod("getView")
             private val notifyResizedMethod =
                 targetClass.getMethod("notifyResized", Int::class.java, Int::class.java)
+            private val getSignalOptionsMethod = targetClass.getMethod("getSignalOptions")
             private val notifyZOrderChangedMethod =
                 targetClass.getMethod("notifyZOrderChanged", Boolean::class.java)
             private val notifyConfigurationChangedMethod =
                 targetClass.getMethod("notifyConfigurationChanged", Configuration::class.java)
+            private val notifyUiChangedMethod =
+                targetClass.getMethod("notifyUiChanged", Bundle::class.java)
             private val closeMethod = targetClass.getMethod("close")
 
             override val view: View
-                @SuppressLint("BanUncheckedReflection") // using reflection on library classes
                 get() = getViewMethod.invoke(origSession) as View
 
-            @SuppressLint("BanUncheckedReflection") // using reflection on library classes
+            override val signalOptions: Set<String>
+                @Suppress("UNCHECKED_CAST") // using reflection on library classes
+                get() = getSignalOptionsMethod.invoke(origSession) as Set<String>
+
             override fun notifyResized(width: Int, height: Int) {
                 view.layout(0, 0, width, height)
                 notifyResizedMethod.invoke(origSession, width, height)
             }
 
-            @SuppressLint("BanUncheckedReflection") // using reflection on library classes
             override fun notifyZOrderChanged(isZOrderOnTop: Boolean) {
                 notifyZOrderChangedMethod.invoke(origSession, isZOrderOnTop)
             }
 
-            @SuppressLint("BanUncheckedReflection") // using reflection on library classes
             override fun notifyConfigurationChanged(configuration: Configuration) {
                 notifyConfigurationChangedMethod.invoke(origSession, configuration)
             }
 
-            @SuppressLint("BanUncheckedReflection") // using reflection on library classes
+            override fun notifyUiChanged(uiContainerInfo: Bundle) {
+                notifyUiChangedMethod.invoke(origSession, uiContainerInfo)
+            }
+
             override fun close() {
                 closeMethod.invoke(origSession)
             }
@@ -261,6 +271,10 @@
             }
         }
 
+        override fun addObserverFactory(sessionObserverFactory: SessionObserverFactory) {}
+
+        override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) {}
+
         class RemoteSessionClient(
             val context: Context,
             val client: SandboxedUiAdapter.SessionClient,
@@ -272,7 +286,8 @@
             override fun onRemoteSessionOpened(
                 surfacePackage: SurfaceControlViewHost.SurfacePackage,
                 remoteSessionController: IRemoteSessionController,
-                isZOrderOnTop: Boolean
+                isZOrderOnTop: Boolean,
+                hasObservers: Boolean
             ) {
                 surfaceView = SurfaceView(context)
                 surfaceView.setChildSurfacePackage(surfacePackage)
@@ -298,7 +313,12 @@
 
                 clientExecutor.execute {
                     client.onSessionOpened(
-                        SessionImpl(surfaceView, remoteSessionController, surfacePackage)
+                        SessionImpl(
+                            surfaceView,
+                            remoteSessionController,
+                            surfacePackage,
+                            hasObservers
+                        )
                     )
                 }
                 tryToCallRemoteObject {
@@ -324,11 +344,22 @@
         private class SessionImpl(
             val surfaceView: SurfaceView,
             val remoteSessionController: IRemoteSessionController,
-            val surfacePackage: SurfaceControlViewHost.SurfacePackage
+            val surfacePackage: SurfaceControlViewHost.SurfacePackage,
+            hasObservers: Boolean
         ) : SandboxedUiAdapter.Session {
 
             override val view: View = surfaceView
 
+            // While there are no more refined signal options, just use hasObservers as a signal
+            // for whether to start measurement.
+            // TODO(b/341895747): Add structured signal options.
+            override val signalOptions =
+                if (hasObservers) {
+                    setOf("someOptions")
+                } else {
+                    setOf()
+                }
+
             override fun notifyConfigurationChanged(configuration: Configuration) {
                 tryToCallRemoteObject {
                     remoteSessionController.notifyConfigurationChanged(configuration)
@@ -363,6 +394,10 @@
                 tryToCallRemoteObject { remoteSessionController.notifyZOrderChanged(isZOrderOnTop) }
             }
 
+            override fun notifyUiChanged(uiContainerInfo: Bundle) {
+                tryToCallRemoteObject { remoteSessionController.notifyUiChanged(uiContainerInfo) }
+            }
+
             override fun close() {
                 tryToCallRemoteObject { remoteSessionController.close() }
             }
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
index d566994..061bf98 100644
--- a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkView.kt
@@ -106,6 +106,7 @@
             override fun surfaceCreated(p0: SurfaceHolder) {
                 updateAndSetClippingBounds(true)
                 viewTreeObserver.addOnGlobalLayoutListener(globalLayoutChangeListener)
+                viewTreeObserver.addOnScrollChangedListener(scrollChangedListener)
             }
 
             override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) {}
@@ -117,6 +118,9 @@
     private val globalLayoutChangeListener =
         ViewTreeObserver.OnGlobalLayoutListener { updateAndSetClippingBounds() }
 
+    private val scrollChangedListener =
+        ViewTreeObserver.OnScrollChangedListener { signalMeasurer?.maybeSendSignals() }
+
     private var adapter: SandboxedUiAdapter? = null
     private var client: Client? = null
     private var isZOrderOnTop = true
@@ -131,6 +135,7 @@
     internal val stateListenerManager: StateListenerManager = StateListenerManager()
     private var viewContainingPoolingContainerListener: View? = null
     private var poolingContainerListener = PoolingContainerListener {}
+    internal var signalMeasurer: SandboxedSdkViewSignalMeasurer? = null
 
     /** Adds a state change listener to the UI session and immediately reports the current state. */
     fun addStateChangedListener(stateChangedListener: SandboxedSdkUiSessionStateChangedListener) {
@@ -148,6 +153,7 @@
         if (this.adapter === sandboxedUiAdapter) return
         client?.close()
         client = null
+        signalMeasurer = null
         this.adapter = sandboxedUiAdapter
         checkClientOpenSession()
     }
@@ -225,7 +231,6 @@
     }
 
     private fun removeContentView() {
-        removeCallbacks()
         if (childCount == 1) {
             super.removeViewAt(0)
         }
@@ -234,6 +239,7 @@
     private fun removeCallbacks() {
         (contentView as? SurfaceView)?.holder?.removeCallback(surfaceChangedCallback)
         viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutChangeListener)
+        viewTreeObserver.removeOnScrollChangedListener(scrollChangedListener)
     }
 
     internal fun setContentView(contentView: View) {
@@ -251,6 +257,7 @@
         }
 
         // Wait for the next frame commit before sending an ACTIVE state change to listeners.
+        // TODO(b/338196636): Unregister this when necessary.
         CompatImpl.registerFrameCommitCallback(viewTreeObserver) {
             stateListenerManager.currentUiSessionState = Active
         }
@@ -262,6 +269,8 @@
 
     internal fun onClientClosedSession(error: Throwable? = null) {
         removeContentView()
+        signalMeasurer?.dropPendingUpdates()
+        signalMeasurer = null
         stateListenerManager.currentUiSessionState =
             if (error != null) {
                 SandboxedSdkUiSessionState.Error(error)
@@ -339,6 +348,12 @@
         previousHeight = height
         previousWidth = width
         checkClientOpenSession()
+        signalMeasurer?.maybeSendSignals()
+    }
+
+    override fun setAlpha(alpha: Float) {
+        super.setAlpha(alpha)
+        signalMeasurer?.maybeSendSignals()
     }
 
     private fun closeClient() {
@@ -388,6 +403,7 @@
     }
 
     override fun onDetachedFromWindow() {
+        removeCallbacks()
         if (!this.isWithinPoolingContainer) {
             closeClient()
         }
@@ -501,7 +517,8 @@
                 session.close()
                 return
             }
-            sandboxedSdkView?.setContentView(session.view)
+            val view = checkNotNull(sandboxedSdkView) { "SandboxedSdkView should not be null" }
+            view.setContentView(session.view)
             this.session = session
             val width = pendingWidth
             val height = pendingHeight
@@ -512,6 +529,9 @@
             pendingConfiguration = null
             pendingZOrderOnTop?.let { session.notifyZOrderChanged(it) }
             pendingZOrderOnTop = null
+            if (session.signalOptions.isNotEmpty()) {
+                view.signalMeasurer = SandboxedSdkViewSignalMeasurer(view, session)
+            }
         }
 
         override fun onSessionError(throwable: Throwable) {
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkViewSignalMeasurer.kt b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkViewSignalMeasurer.kt
new file mode 100644
index 0000000..57d201a
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/client/view/SandboxedSdkViewSignalMeasurer.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.client.view
+
+import android.graphics.Rect
+import android.os.SystemClock
+import androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo
+import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+
+/**
+ * Class for calculating signals related to the presentation of a [SandboxedSdkView].
+ *
+ * This class also schedules the collection of signals to ensure that signals are not sent too
+ * frequently.
+ */
+internal class SandboxedSdkViewSignalMeasurer(
+    val view: SandboxedSdkView,
+    private val session: SandboxedUiAdapter.Session,
+    private val clock: Clock = Clock { SystemClock.uptimeMillis() }
+) {
+    private companion object {
+        private const val MIN_SIGNAL_LATENCY_MS = 200
+    }
+
+    internal fun interface Clock {
+        fun uptimeMillis(): Long
+    }
+
+    private val windowLocation = IntArray(2)
+    private var onScreenGeometry = Rect()
+    private var containerWidthPx = 0
+    private var containerHeightPx = 0
+    private var opacityHint = 1.0f
+    private var lastTimeSentSignals: Long = clock.uptimeMillis()
+    private var scheduledTask: Runnable? = null
+
+    /**
+     * Updates the [SandboxedSdkViewUiInfo] that represents the view if there is no task already
+     * scheduled and the time since the last time signals were sent is at least the minimum
+     * acceptable latency.
+     *
+     * TODO(b/333853853): Use concurrency constructs instead.
+     */
+    fun maybeSendSignals() {
+        if (scheduledTask != null) {
+            return
+        }
+
+        if ((clock.uptimeMillis() - lastTimeSentSignals) < MIN_SIGNAL_LATENCY_MS) {
+            val delayToNextSend =
+                MIN_SIGNAL_LATENCY_MS - (clock.uptimeMillis() - lastTimeSentSignals)
+            scheduledTask = Runnable {
+                scheduledTask = null
+                maybeSendSignals()
+            }
+            view.postDelayed(scheduledTask, delayToNextSend)
+        } else {
+            updateUiContainerInfo()
+            session.notifyUiChanged(
+                SandboxedSdkViewUiInfo.toBundle(
+                    SandboxedSdkViewUiInfo(
+                        containerWidthPx,
+                        containerHeightPx,
+                        onScreenGeometry,
+                        opacityHint
+                    )
+                )
+            )
+            lastTimeSentSignals = clock.uptimeMillis()
+        }
+    }
+
+    /** Removes the pending UI update [Runnable] from the message queue, if one exists. */
+    fun dropPendingUpdates() {
+        scheduledTask?.let { view.removeCallbacks(it) }
+        scheduledTask = null
+    }
+
+    /** Updates the [SandboxedSdkViewUiInfo] that represents the state of the view. */
+    private fun updateUiContainerInfo() {
+        val isVisible = view.getGlobalVisibleRect(onScreenGeometry)
+        if (!isVisible) {
+            onScreenGeometry.set(-1, -1, -1, -1)
+        } else {
+            view.getLocationOnScreen(windowLocation)
+            onScreenGeometry.offset(-windowLocation[0], -windowLocation[1])
+            onScreenGeometry.intersect(0, 0, view.width, view.height)
+        }
+        containerHeightPx = view.height
+        containerWidthPx = view.width
+        opacityHint = view.alpha
+    }
+}
diff --git a/privacysandbox/ui/ui-core/api/current.txt b/privacysandbox/ui/ui-core/api/current.txt
index 5ab678d..aa33944 100644
--- a/privacysandbox/ui/ui-core/api/current.txt
+++ b/privacysandbox/ui/ui-core/api/current.txt
@@ -1,16 +1,41 @@
 // Signature format: 4.0
 package androidx.privacysandbox.ui.core {
 
+  public final class SandboxedSdkViewUiInfo {
+    ctor public SandboxedSdkViewUiInfo(int uiContainerWidth, int uiContainerHeight, android.graphics.Rect onScreenGeometry, float uiContainerOpacityHint);
+    method public static androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo fromBundle(android.os.Bundle bundle);
+    method public android.graphics.Rect getOnScreenGeometry();
+    method public int getUiContainerHeight();
+    method public float getUiContainerOpacityHint();
+    method public int getUiContainerWidth();
+    method public static android.os.Bundle toBundle(androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo sandboxedSdkViewUiInfo);
+    property public final android.graphics.Rect onScreenGeometry;
+    property public final int uiContainerHeight;
+    property public final float uiContainerOpacityHint;
+    property public final int uiContainerWidth;
+    field public static final androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo.Companion Companion;
+  }
+
+  public static final class SandboxedSdkViewUiInfo.Companion {
+    method public androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo fromBundle(android.os.Bundle bundle);
+    method public android.os.Bundle toBundle(androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo sandboxedSdkViewUiInfo);
+  }
+
   public interface SandboxedUiAdapter {
+    method public void addObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
     method public void openSession(android.content.Context context, android.os.IBinder windowInputToken, int initialWidth, int initialHeight, boolean isZOrderOnTop, java.util.concurrent.Executor clientExecutor, androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient client);
+    method public void removeObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
   }
 
   public static interface SandboxedUiAdapter.Session extends java.lang.AutoCloseable {
     method public void close();
+    method public java.util.Set<java.lang.String> getSignalOptions();
     method public android.view.View getView();
     method public void notifyConfigurationChanged(android.content.res.Configuration configuration);
     method public void notifyResized(int width, int height);
+    method public void notifyUiChanged(android.os.Bundle uiContainerInfo);
     method public void notifyZOrderChanged(boolean isZOrderOnTop);
+    property public abstract java.util.Set<java.lang.String> signalOptions;
     property public abstract android.view.View view;
   }
 
@@ -31,5 +56,21 @@
     field public static final int apiVersion = 1; // 0x1
   }
 
+  public interface SessionObserver {
+    method public void onSessionClosed();
+    method public void onSessionOpened(androidx.privacysandbox.ui.core.SessionObserverContext sessionObserverContext);
+    method public void onUiContainerChanged(android.os.Bundle uiContainerInfo);
+  }
+
+  public final class SessionObserverContext {
+    ctor public SessionObserverContext(android.view.View? view);
+    method public android.view.View? getView();
+    property public final android.view.View? view;
+  }
+
+  public fun interface SessionObserverFactory {
+    method public androidx.privacysandbox.ui.core.SessionObserver create();
+  }
+
 }
 
diff --git a/privacysandbox/ui/ui-core/api/restricted_current.txt b/privacysandbox/ui/ui-core/api/restricted_current.txt
index 5ab678d..aa33944 100644
--- a/privacysandbox/ui/ui-core/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-core/api/restricted_current.txt
@@ -1,16 +1,41 @@
 // Signature format: 4.0
 package androidx.privacysandbox.ui.core {
 
+  public final class SandboxedSdkViewUiInfo {
+    ctor public SandboxedSdkViewUiInfo(int uiContainerWidth, int uiContainerHeight, android.graphics.Rect onScreenGeometry, float uiContainerOpacityHint);
+    method public static androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo fromBundle(android.os.Bundle bundle);
+    method public android.graphics.Rect getOnScreenGeometry();
+    method public int getUiContainerHeight();
+    method public float getUiContainerOpacityHint();
+    method public int getUiContainerWidth();
+    method public static android.os.Bundle toBundle(androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo sandboxedSdkViewUiInfo);
+    property public final android.graphics.Rect onScreenGeometry;
+    property public final int uiContainerHeight;
+    property public final float uiContainerOpacityHint;
+    property public final int uiContainerWidth;
+    field public static final androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo.Companion Companion;
+  }
+
+  public static final class SandboxedSdkViewUiInfo.Companion {
+    method public androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo fromBundle(android.os.Bundle bundle);
+    method public android.os.Bundle toBundle(androidx.privacysandbox.ui.core.SandboxedSdkViewUiInfo sandboxedSdkViewUiInfo);
+  }
+
   public interface SandboxedUiAdapter {
+    method public void addObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
     method public void openSession(android.content.Context context, android.os.IBinder windowInputToken, int initialWidth, int initialHeight, boolean isZOrderOnTop, java.util.concurrent.Executor clientExecutor, androidx.privacysandbox.ui.core.SandboxedUiAdapter.SessionClient client);
+    method public void removeObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
   }
 
   public static interface SandboxedUiAdapter.Session extends java.lang.AutoCloseable {
     method public void close();
+    method public java.util.Set<java.lang.String> getSignalOptions();
     method public android.view.View getView();
     method public void notifyConfigurationChanged(android.content.res.Configuration configuration);
     method public void notifyResized(int width, int height);
+    method public void notifyUiChanged(android.os.Bundle uiContainerInfo);
     method public void notifyZOrderChanged(boolean isZOrderOnTop);
+    property public abstract java.util.Set<java.lang.String> signalOptions;
     property public abstract android.view.View view;
   }
 
@@ -31,5 +56,21 @@
     field public static final int apiVersion = 1; // 0x1
   }
 
+  public interface SessionObserver {
+    method public void onSessionClosed();
+    method public void onSessionOpened(androidx.privacysandbox.ui.core.SessionObserverContext sessionObserverContext);
+    method public void onUiContainerChanged(android.os.Bundle uiContainerInfo);
+  }
+
+  public final class SessionObserverContext {
+    ctor public SessionObserverContext(android.view.View? view);
+    method public android.view.View? getView();
+    property public final android.view.View? view;
+  }
+
+  public fun interface SessionObserverFactory {
+    method public androidx.privacysandbox.ui.core.SessionObserver create();
+  }
+
 }
 
diff --git a/privacysandbox/ui/ui-core/build.gradle b/privacysandbox/ui/ui-core/build.gradle
index 9ea0d9d..e6df2ad 100644
--- a/privacysandbox/ui/ui-core/build.gradle
+++ b/privacysandbox/ui/ui-core/build.gradle
@@ -39,6 +39,7 @@
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.testRules)
     androidTestImplementation(libs.truth)
+    implementation("androidx.core:core:1.13.0")
 }
 
 android {
diff --git a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionClient.aidl b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionClient.aidl
index a28c0a7..bd085ca 100644
--- a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionClient.aidl
+++ b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionClient.aidl
@@ -22,7 +22,8 @@
 @JavaPassthrough(annotation="@androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)")
 oneway interface IRemoteSessionClient {
     void onRemoteSessionOpened(in SurfacePackage surfacePackage,
-        IRemoteSessionController remoteSessionController, boolean isZOrderOnTop);
+        IRemoteSessionController remoteSessionController,
+        boolean isZOrderOnTop, boolean hasObservers);
     void onRemoteSessionError(String exception);
     void onResizeRequested(int width, int height);
     void onSessionUiFetched(in SurfacePackage surfacePackage);
diff --git a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
index c685b2b..704c474 100644
--- a/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
+++ b/privacysandbox/ui/ui-core/src/main/aidl/androidx/privacysandbox/ui/core/IRemoteSessionController.aidl
@@ -23,4 +23,5 @@
     void notifyResized(int width, int height);
     void notifyZOrderChanged(boolean isZOrderOnTop);
     void notifyFetchUiForSession();
+    void notifyUiChanged(in Bundle uiContainerInfo);
 }
\ No newline at end of file
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedSdkViewUiInfo.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedSdkViewUiInfo.kt
new file mode 100644
index 0000000..8aa2449
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedSdkViewUiInfo.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.core
+
+import android.graphics.Rect
+import android.os.Bundle
+import androidx.core.os.BundleCompat
+
+/** A class representing the UI state of a SandboxedSdkView. */
+class SandboxedSdkViewUiInfo(
+    /** Returns the width of the UI container in pixels. */
+    val uiContainerWidth: Int,
+    /** Returns the height of the UI container in pixels. */
+    val uiContainerHeight: Int,
+    /**
+     * Returns the portion of the UI container which is not clipped by parent views and is visible
+     * on screen. The coordinates of this [Rect] are relative to the UI container and measured in
+     * pixels.
+     *
+     * If none of the UI container is visible on screen, each coordinate in this [Rect] will be -1.
+     */
+    val onScreenGeometry: Rect,
+    /**
+     * Returns the opacity of the UI container, where available.
+     *
+     * When available, this is a value from 0 to 1, where 0 means the container is completely
+     * transparent and 1 means the container is completely opaque. This value doesn't necessarily
+     * reflect the user-visible opacity of the UI container, as shaders and other overlays can
+     * affect that.
+     *
+     * When the opacity is not available, the value will be -1.
+     */
+    val uiContainerOpacityHint: Float
+) {
+    companion object {
+        private const val UI_CONTAINER_WIDTH_KEY = "uiContainerWidth"
+        private const val UI_CONTAINER_HEIGHT_KEY = "uiContainerHeight"
+        private const val ONSCREEN_GEOMETRY_KEY = "onScreenGeometry"
+        private const val UI_CONTAINER_OPACITY_KEY = "uiContainerOpacity"
+
+        @JvmStatic
+        fun fromBundle(bundle: Bundle): SandboxedSdkViewUiInfo {
+            val uiContainerWidth = bundle.getInt(UI_CONTAINER_WIDTH_KEY)
+            val uiContainerHeight = bundle.getInt(UI_CONTAINER_HEIGHT_KEY)
+            val onScreenGeometry =
+                checkNotNull(
+                    BundleCompat.getParcelable(bundle, ONSCREEN_GEOMETRY_KEY, Rect::class.java)
+                )
+            val uiContainerOpacityHint = bundle.getFloat(UI_CONTAINER_OPACITY_KEY)
+            return SandboxedSdkViewUiInfo(
+                uiContainerWidth,
+                uiContainerHeight,
+                onScreenGeometry,
+                uiContainerOpacityHint
+            )
+        }
+
+        @JvmStatic
+        fun toBundle(sandboxedSdkViewUiInfo: SandboxedSdkViewUiInfo): Bundle {
+            val bundle = Bundle()
+            bundle.putInt(UI_CONTAINER_WIDTH_KEY, sandboxedSdkViewUiInfo.uiContainerWidth)
+            bundle.putInt(UI_CONTAINER_HEIGHT_KEY, sandboxedSdkViewUiInfo.uiContainerHeight)
+            bundle.putParcelable(ONSCREEN_GEOMETRY_KEY, sandboxedSdkViewUiInfo.onScreenGeometry)
+            bundle.putFloat(UI_CONTAINER_OPACITY_KEY, sandboxedSdkViewUiInfo.uiContainerOpacityHint)
+            return bundle
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is SandboxedSdkViewUiInfo) return false
+
+        return onScreenGeometry == other.onScreenGeometry &&
+            uiContainerWidth == other.uiContainerWidth &&
+            uiContainerHeight == other.uiContainerHeight &&
+            uiContainerOpacityHint == other.uiContainerOpacityHint
+    }
+
+    override fun hashCode(): Int {
+        var result = uiContainerWidth
+        result = 31 * result + uiContainerHeight
+        result = 31 * result + onScreenGeometry.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "SandboxedSdkViewUiInfo(" +
+            "uiContainerWidth=$uiContainerWidth, " +
+            "uiContainerHeight=$uiContainerHeight, " +
+            "onScreenGeometry=$onScreenGeometry," +
+            "uiContainerOpacityHint=$uiContainerOpacityHint"
+    }
+}
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt
index d312c56..ea86d40 100644
--- a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SandboxedUiAdapter.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.content.res.Configuration
+import android.os.Bundle
 import android.os.IBinder
 import android.view.View
 import java.lang.AutoCloseable
@@ -45,6 +46,31 @@
         client: SessionClient
     )
 
+    /**
+     * Adds a [SessionObserverFactory] with a [SandboxedUiAdapter] for tracking UI presentation
+     * state across UI sessions. This has no effect on already open sessions.
+     *
+     * For each [SandboxedUiAdapter.Session] that is created for the adapter after registration is
+     * complete, [SessionObserverFactory.create] will be invoked to allow a new [SessionObserver]
+     * instance to be attached to the UI session. This [SessionObserver] will receive UI updates for
+     * the lifetime of the session. There may be one or more UI sessions created for a
+     * [SandboxedUiAdapter], and a separate [SessionObserverFactory.create] call will be made for
+     * each one.
+     */
+    fun addObserverFactory(sessionObserverFactory: SessionObserverFactory)
+
+    /**
+     * Removes a [SessionObserverFactory] from a [SandboxedUiAdapter], if it has been previously
+     * added with [addObserverFactory].
+     *
+     * If the [SessionObserverFactory] was not previously added, no action is performed. Any
+     * existing [SessionObserver] instances that have been created by the [SessionObserverFactory]
+     * will continue to receive updates until their corresponding [SandboxedUiAdapter.Session] has
+     * been closed. For any subsequent sessions created for the [SandboxedUiAdapter], no call to
+     * [SessionObserverFactory.create] will be made.
+     */
+    fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory)
+
     /** A single session with the provider of remote content. */
     interface Session : AutoCloseable {
 
@@ -56,6 +82,16 @@
         val view: View
 
         /**
+         * The set of options that will be used to determine what information is calculated and sent
+         * to [SessionObserver]s attached to this session.
+         *
+         * This value should not be directly set by UI providers. Instead, the registration of any
+         * [SessionObserverFactory] with [addObserverFactory] will indicate that information should
+         * be calculated for this session.
+         */
+        val signalOptions: Set<String>
+
+        /**
          * Notify the provider that the size of the host presentation area has changed to a size of
          * [width] x [height] pixels.
          */
@@ -71,6 +107,19 @@
         fun notifyConfigurationChanged(configuration: Configuration)
 
         /**
+         * Notify the session when the presentation state of its UI container has changed.
+         *
+         * [uiContainerInfo] contains a Bundle that represents the state of the container. The exact
+         * details of this Bundle depend on the container this Bundle is describing. This
+         * notification is not in real time and is throttled, so it should not be used to react to
+         * UI changes on the client side.
+         *
+         * UI providers should use [addObserverFactory] to observe UI changes rather than using this
+         * method.
+         */
+        fun notifyUiChanged(uiContainerInfo: Bundle)
+
+        /**
          * Close this session, indicating that the remote provider of content should dispose of
          * associated resources and that the [SessionClient] should not receive further callback
          * events.
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserver.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserver.kt
new file mode 100644
index 0000000..6348aa2
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserver.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.core
+
+import android.os.Bundle
+
+/**
+ * An interface that can be used by the client of a
+ * [androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session] to receive updates about its state.
+ * One session may be associated with multiple session observers.
+ *
+ * When a new UI session is started, [onSessionOpened] will be invoked for all registered observers.
+ * This callback will contain a [SessionObserverContext] object containing data which will be
+ * constant for the lifetime of the UI session. [onUiContainerChanged] will also be sent when a new
+ * session is opened. This callback will contain a [Bundle] representing the UI presentation of the
+ * session's view.
+ *
+ * During the entire lifetime of the UI session, [onUiContainerChanged] will be sent when the UI
+ * presentation has changed. These updates will be throttled.
+ *
+ * When the UI session has completed, [onSessionClosed] will be sent. After this point, no more
+ * callbacks will be sent and it is safe to free any resources associated with this session
+ * observer.
+ */
+@SuppressWarnings("CallbackName")
+interface SessionObserver {
+
+    /**
+     * Called exactly once per session, when the
+     * [androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session] associated with the session
+     * observer is created. [sessionObserverContext] contains data which will be constant for the
+     * lifetime of the UI session. The resources associated with the [sessionObserverContext] will
+     * be released when [onSessionClosed] is called.
+     */
+    fun onSessionOpened(sessionObserverContext: SessionObserverContext)
+
+    /**
+     * Called when the UI container has changed its presentation state. Note that these updates will
+     * not be sent instantaneously, and will be throttled. This should not be used to react to UI
+     * changes on the client side as it is not sent in real time.
+     */
+    // TODO(b/326942993): Decide on the correct data type to send.
+    fun onUiContainerChanged(uiContainerInfo: android.os.Bundle)
+
+    /**
+     * Called exactly once per session when the
+     * [androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session] associated with this session
+     * observer closes. No more callbacks will be made on this observer after this point, and any
+     * resources associated with it can be freed.
+     */
+    fun onSessionClosed()
+}
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserverContext.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserverContext.kt
new file mode 100644
index 0000000..99389c7
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserverContext.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.core
+
+import android.view.View
+
+/**
+ * A class containing information that will be constant through the lifetime of a [SessionObserver].
+ *
+ * When [SessionObserver.onSessionClosed] is called for the associated session observers, the
+ * resources of the [SessionObserverContext] will be freed.
+ */
+class SessionObserverContext(
+    /**
+     * Returns the view that is presenting content for the associated [SandboxedUiAdapter.Session].
+     *
+     * This value will be non-null if the [SandboxedUiAdapter.Session] and the [SessionObserver] are
+     * created from the same process. Otherwise, it will be null.
+     */
+    val view: View?
+) {
+    override fun toString() = "SessionObserverContext(view=$view)"
+
+    override fun equals(other: Any?): Boolean {
+        return other is SessionObserverContext && view == other.view
+    }
+
+    override fun hashCode(): Int {
+        return view.hashCode()
+    }
+}
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserverFactory.kt b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserverFactory.kt
new file mode 100644
index 0000000..6597769
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/core/SessionObserverFactory.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.ui.core
+
+/**
+ * A factory that creates [SessionObserver] instances that can be attached to a
+ * [SandboxedUiAdapter.Session]. Many [SessionObserver]s may be created for the same
+ * [SandboxedUiAdapter.Session].
+ */
+fun interface SessionObserverFactory {
+    /**
+     * Called if a new [SandboxedUiAdapter.Session] has been opened by the [SandboxedUiAdapter] that
+     * this factory is registered to. This will not be called for sessions that are already open.
+     */
+    fun create(): SessionObserver
+}
diff --git a/privacysandbox/ui/ui-provider/api/current.txt b/privacysandbox/ui/ui-provider/api/current.txt
index b1deb894..53933f1 100644
--- a/privacysandbox/ui/ui-provider/api/current.txt
+++ b/privacysandbox/ui/ui-provider/api/current.txt
@@ -3,13 +3,21 @@
 
   public abstract class AbstractSandboxedUiAdapter implements androidx.privacysandbox.ui.core.SandboxedUiAdapter {
     ctor public AbstractSandboxedUiAdapter();
+    method public final void addObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
+    method public final java.util.List<androidx.privacysandbox.ui.core.SessionObserverFactory> getSessionObserverFactories();
+    method public final void removeObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
+    property public final java.util.List<androidx.privacysandbox.ui.core.SessionObserverFactory> sessionObserverFactories;
   }
 
   public abstract static class AbstractSandboxedUiAdapter.AbstractSession implements androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session {
     ctor public AbstractSandboxedUiAdapter.AbstractSession();
+    method public void close();
+    method public final java.util.Set<java.lang.String> getSignalOptions();
     method public void notifyConfigurationChanged(android.content.res.Configuration configuration);
     method public void notifyResized(int width, int height);
+    method public void notifyUiChanged(android.os.Bundle uiContainerInfo);
     method public void notifyZOrderChanged(boolean isZOrderOnTop);
+    property public final java.util.Set<java.lang.String> signalOptions;
   }
 
   public final class SandboxedUiAdapterProxy {
diff --git a/privacysandbox/ui/ui-provider/api/restricted_current.txt b/privacysandbox/ui/ui-provider/api/restricted_current.txt
index b1deb894..53933f1 100644
--- a/privacysandbox/ui/ui-provider/api/restricted_current.txt
+++ b/privacysandbox/ui/ui-provider/api/restricted_current.txt
@@ -3,13 +3,21 @@
 
   public abstract class AbstractSandboxedUiAdapter implements androidx.privacysandbox.ui.core.SandboxedUiAdapter {
     ctor public AbstractSandboxedUiAdapter();
+    method public final void addObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
+    method public final java.util.List<androidx.privacysandbox.ui.core.SessionObserverFactory> getSessionObserverFactories();
+    method public final void removeObserverFactory(androidx.privacysandbox.ui.core.SessionObserverFactory sessionObserverFactory);
+    property public final java.util.List<androidx.privacysandbox.ui.core.SessionObserverFactory> sessionObserverFactories;
   }
 
   public abstract static class AbstractSandboxedUiAdapter.AbstractSession implements androidx.privacysandbox.ui.core.SandboxedUiAdapter.Session {
     ctor public AbstractSandboxedUiAdapter.AbstractSession();
+    method public void close();
+    method public final java.util.Set<java.lang.String> getSignalOptions();
     method public void notifyConfigurationChanged(android.content.res.Configuration configuration);
     method public void notifyResized(int width, int height);
+    method public void notifyUiChanged(android.os.Bundle uiContainerInfo);
     method public void notifyZOrderChanged(boolean isZOrderOnTop);
+    property public final java.util.Set<java.lang.String> signalOptions;
   }
 
   public final class SandboxedUiAdapterProxy {
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/AbstractSandboxedUiAdapter.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/AbstractSandboxedUiAdapter.kt
index 168541f..acf6d521 100644
--- a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/AbstractSandboxedUiAdapter.kt
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/AbstractSandboxedUiAdapter.kt
@@ -17,7 +17,9 @@
 package androidx.privacysandbox.ui.provider
 
 import android.content.res.Configuration
+import android.os.Bundle
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SessionObserverFactory
 
 /**
  * An abstract class that implements [SandboxedUiAdapter] while abstracting away methods that do not
@@ -27,6 +29,28 @@
  */
 abstract class AbstractSandboxedUiAdapter : SandboxedUiAdapter {
 
+    /** The list of [SessionObserverFactory] instances that have been added to this adapter. */
+    val sessionObserverFactories: List<SessionObserverFactory>
+        get() {
+            synchronized(_sessionObserverFactories) {
+                return _sessionObserverFactories.toList()
+            }
+        }
+
+    private val _sessionObserverFactories: MutableList<SessionObserverFactory> = mutableListOf()
+
+    final override fun addObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+        synchronized(_sessionObserverFactories) {
+            _sessionObserverFactories.add(sessionObserverFactory)
+        }
+    }
+
+    final override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) {
+        synchronized(_sessionObserverFactories) {
+            _sessionObserverFactories.remove(sessionObserverFactory)
+        }
+    }
+
     /**
      * An abstract class that implements [SandboxedUiAdapter.Session] so that a UI provider does not
      * need to implement the entire interface.
@@ -35,10 +59,17 @@
      */
     abstract class AbstractSession : SandboxedUiAdapter.Session {
 
+        final override val signalOptions: Set<String>
+            get() = setOf()
+
         override fun notifyZOrderChanged(isZOrderOnTop: Boolean) {}
 
         override fun notifyResized(width: Int, height: Int) {}
 
         override fun notifyConfigurationChanged(configuration: Configuration) {}
+
+        override fun notifyUiChanged(uiContainerInfo: Bundle) {}
+
+        override fun close() {}
     }
 }
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
index 91f074e..a993e74 100644
--- a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/provider/BinderAdapterDelegate.kt
@@ -28,6 +28,7 @@
 import android.util.Log
 import android.view.Display
 import android.view.SurfaceControlViewHost
+import android.view.View
 import androidx.annotation.DoNotInline
 import androidx.annotation.RequiresApi
 import androidx.annotation.VisibleForTesting
@@ -35,6 +36,9 @@
 import androidx.privacysandbox.ui.core.IRemoteSessionController
 import androidx.privacysandbox.ui.core.ISandboxedUiAdapter
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SessionObserver
+import androidx.privacysandbox.ui.core.SessionObserverContext
+import androidx.privacysandbox.ui.core.SessionObserverFactory
 import java.util.concurrent.Executor
 
 /**
@@ -76,10 +80,14 @@
             initialHeight,
             isZOrderOnTop,
             clientExecutor,
-            client
+            SessionClientForObservers(client)
         )
     }
 
+    override fun addObserverFactory(sessionObserverFactory: SessionObserverFactory) {}
+
+    override fun removeObserverFactory(sessionObserverFactory: SessionObserverFactory) {}
+
     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     override fun openRemoteSession(
         windowInputToken: IBinder,
@@ -183,7 +191,8 @@
             remoteSessionClient.onRemoteSessionOpened(
                 surfacePackage,
                 remoteSessionController,
-                isZOrderOnTop
+                isZOrderOnTop,
+                session.signalOptions.isNotEmpty()
             )
         }
 
@@ -196,11 +205,10 @@
         @VisibleForTesting
         private inner class RemoteSessionController(
             val surfaceControlViewHost: SurfaceControlViewHost,
-            val session: SandboxedUiAdapter.Session
+            val session: SandboxedUiAdapter.Session,
         ) : IRemoteSessionController.Stub() {
 
             override fun notifyConfigurationChanged(configuration: Configuration) {
-                surfaceControlViewHost.surfacePackage?.notifyConfigurationChanged(configuration)
                 session.notifyConfigurationChanged(configuration)
             }
 
@@ -220,6 +228,10 @@
                 sendSurfacePackage()
             }
 
+            override fun notifyUiChanged(uiContainerInfo: Bundle) {
+                session.notifyUiChanged(uiContainerInfo)
+            }
+
             override fun close() {
                 val mHandler = Handler(Looper.getMainLooper())
                 mHandler.post {
@@ -231,6 +243,59 @@
     }
 
     /**
+     * Wrapper class to handle the creation of [SessionObserver] instances when the session is
+     * opened.
+     */
+    private inner class SessionClientForObservers(val client: SandboxedUiAdapter.SessionClient) :
+        SandboxedUiAdapter.SessionClient by client {
+
+        override fun onSessionOpened(session: SandboxedUiAdapter.Session) {
+            val sessionObservers: MutableList<SessionObserver> = mutableListOf()
+            if (adapter is AbstractSandboxedUiAdapter) {
+                adapter.sessionObserverFactories.forEach { sessionObservers.add(it.create()) }
+            }
+            client.onSessionOpened(SessionForObservers(session, sessionObservers))
+        }
+    }
+
+    /**
+     * Wrapper class of a [SandboxedUiAdapter.Session] that handles the sending of events to any
+     * [SessionObserver]s attached to the session.
+     */
+    private class SessionForObservers(
+        val session: SandboxedUiAdapter.Session,
+        val sessionObservers: List<SessionObserver>
+    ) : SandboxedUiAdapter.Session by session {
+
+        init {
+            if (sessionObservers.isNotEmpty()) {
+                val sessionObserverContext = SessionObserverContext(view)
+                sessionObservers.forEach { it.onSessionOpened(sessionObserverContext) }
+            }
+        }
+
+        override val view: View
+            get() = session.view
+
+        override val signalOptions: Set<String>
+            get() =
+                if (sessionObservers.isEmpty()) {
+                    setOf()
+                } else {
+                    setOf("someOptions")
+                }
+
+        override fun notifyUiChanged(uiContainerInfo: Bundle) {
+            sessionObservers.forEach { it.onUiContainerChanged(uiContainerInfo) }
+        }
+
+        override fun close() {
+            session.close()
+            sessionObservers.forEach { it.onSessionClosed() }
+        }
+    }
+
+    /**
      * Provides backward compat support for APIs.
      *
      * If the API is available, it's called from a version-specific static inner class gated with
diff --git a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
index b4cf11b..7e17123 100644
--- a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
+++ b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/endtoend/IntegrationTests.kt
@@ -322,7 +322,9 @@
         // Verify toString, hashCode and equals have been implemented for dynamic proxy
         val testSession = sdkAdapter.session as TestSandboxedUiAdapter.TestSession
         val client = testSession.sessionClient
-        assertThat(client.toString()).isEqualTo(testSessionClient.toString())
+
+        // TODO(b/329468679): We cannot assert this as we wrap the client on the provider side.
+        // assertThat(client.toString()).isEqualTo(testSessionClient.toString())
 
         assertThat(client.equals(client)).isTrue()
         assertThat(client).isNotEqualTo(testSessionClient)
@@ -349,6 +351,133 @@
         session.assertViewWasLaidOut()
     }
 
+    @Test
+    fun testAddSessionObserverFactory_ObserverIsCreated() {
+        val factory = TestSessionManager.SessionObserverFactoryImpl()
+        sessionManager.createAdapterAndWaitToBeActive(
+            viewForSession = view,
+            sessionObserverFactories = listOf(factory)
+        )
+        assertThat(factory.sessionObservers.size).isEqualTo(1)
+    }
+
+    @Test
+    fun testAddSessionObserverFactory_OnSessionOpenedIsSent() {
+        val factory = TestSessionManager.SessionObserverFactoryImpl()
+        sessionManager.createAdapterAndWaitToBeActive(
+            viewForSession = view,
+            sessionObserverFactories = listOf(factory)
+        )
+        assertThat(factory.sessionObservers.size).isEqualTo(1)
+        val sessionObserver = factory.sessionObservers[0]
+        sessionObserver.assertSessionOpened()
+    }
+
+    @Test
+    fun testAddSessionObserverFactory_NoObserverCreatedForAlreadyOpenSession() {
+        val adapter = sessionManager.createAdapterAndWaitToBeActive(viewForSession = view)
+        val factory = TestSessionManager.SessionObserverFactoryImpl()
+        adapter.addObserverFactory(factory)
+        factory.assertNoSessionsAreCreated()
+    }
+
+    @Test
+    fun testAddSessionObserverFactory_MultipleFactories() {
+        val factory1 = TestSessionManager.SessionObserverFactoryImpl()
+        val factory2 = TestSessionManager.SessionObserverFactoryImpl()
+        sessionManager.createAdapterAndWaitToBeActive(
+            viewForSession = view,
+            sessionObserverFactories = listOf(factory1, factory2)
+        )
+        assertThat(factory1.sessionObservers.size).isEqualTo(1)
+        assertThat(factory2.sessionObservers.size).isEqualTo(1)
+    }
+
+    @Test
+    fun testAddSessionObserverFactory_SessionObserverContextIsCorrect() {
+        val factory = TestSessionManager.SessionObserverFactoryImpl()
+        val adapter =
+            sessionManager.createAdapterAndWaitToBeActive(
+                viewForSession = view,
+                sessionObserverFactories = listOf(factory)
+            )
+        assertThat(factory.sessionObservers.size).isEqualTo(1)
+        val sessionObserver = factory.sessionObservers[0]
+        sessionObserver.assertSessionOpened()
+        assertThat(sessionObserver.sessionObserverContext).isNotNull()
+        assertThat(sessionObserver.sessionObserverContext?.view).isEqualTo(adapter.session.view)
+    }
+
+    @Test
+    fun testRegisterSessionObserverFactory_OnUiContainerChangedSentWhenSessionOpened() {
+        val factory = TestSessionManager.SessionObserverFactoryImpl()
+        sessionManager.createAdapterAndWaitToBeActive(
+            viewForSession = view,
+            sessionObserverFactories = listOf(factory)
+        )
+        assertThat(factory.sessionObservers.size).isEqualTo(1)
+        val sessionObserver = factory.sessionObservers[0]
+        sessionObserver.assertOnUiContainerChangedSent()
+    }
+
+    @Test
+    fun testRemoveSessionObserverFactory_DoesNotImpactExistingObservers() {
+        val factory = TestSessionManager.SessionObserverFactoryImpl()
+        val adapter =
+            sessionManager.createAdapterAndWaitToBeActive(
+                viewForSession = view,
+                sessionObserverFactories = listOf(factory)
+            )
+        assertThat(factory.sessionObservers.size).isEqualTo(1)
+        adapter.removeObserverFactory(factory)
+        val sessionObserver = factory.sessionObservers[0]
+        // Setting a new adapter on the SandboxedSdKView will cause the current session to close.
+        activityScenarioRule.withActivity { view.setAdapter(TestSandboxedUiAdapter()) }
+        // onSessionClosed is still sent for the observer
+        sessionObserver.assertSessionClosed()
+    }
+
+    @Test
+    fun testRemoveSessionObserverFactory_DoesNotCreateObserverForNewSession() {
+        val factory = TestSessionManager.SessionObserverFactoryImpl()
+        val adapter =
+            sessionManager.createAdapterAndWaitToBeActive(
+                viewForSession = view,
+                sessionObserverFactories = listOf(factory)
+            )
+        assertThat(factory.sessionObservers.size).isEqualTo(1)
+        adapter.removeObserverFactory(factory)
+        val sandboxedSdkView2 = SandboxedSdkView(context)
+        activityScenarioRule.withActivity { linearLayout.addView(sandboxedSdkView2) }
+        // create a new session and wait to be active
+        sandboxedSdkView2.setAdapter(adapter)
+
+        val activeLatch = CountDownLatch(1)
+        sandboxedSdkView2.addStateChangedListener { state ->
+            if (state is SandboxedSdkUiSessionState.Active) {
+                activeLatch.countDown()
+            }
+        }
+        assertThat(activeLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        // The session observers size should remain 1, showing that no new observers have been
+        // created for the new session.
+        assertThat(factory.sessionObservers.size).isEqualTo(1)
+    }
+
+    @Test
+    fun testSessionObserver_OnClosedSentWhenSessionClosed() {
+        val factory = TestSessionManager.SessionObserverFactoryImpl()
+        sessionManager.createAdapterAndWaitToBeActive(
+            viewForSession = view,
+            sessionObserverFactories = listOf(factory)
+        )
+        assertThat(factory.sessionObservers.size).isEqualTo(1)
+        val sessionObserver = factory.sessionObservers[0]
+        // Setting a new adapter on the SandboxedSdKView will cause the current session to close.
+        activityScenarioRule.withActivity { view.setAdapter(TestSandboxedUiAdapter()) }
+        sessionObserver.assertSessionClosed()
+    }
+
     private fun injectInputEventOnView() {
         activityScenarioRule.withActivity {
             val location = IntArray(2)
diff --git a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/util/TestSessionManager.kt b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/util/TestSessionManager.kt
index 7f08d8c..85868f9 100644
--- a/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/util/TestSessionManager.kt
+++ b/privacysandbox/ui/ui-tests/src/androidTest/java/androidx/privacysandbox/ui/tests/util/TestSessionManager.kt
@@ -29,6 +29,9 @@
 import androidx.privacysandbox.ui.client.view.SandboxedSdkUiSessionState
 import androidx.privacysandbox.ui.client.view.SandboxedSdkView
 import androidx.privacysandbox.ui.core.SandboxedUiAdapter
+import androidx.privacysandbox.ui.core.SessionObserver
+import androidx.privacysandbox.ui.core.SessionObserverContext
+import androidx.privacysandbox.ui.core.SessionObserverFactory
 import androidx.privacysandbox.ui.provider.AbstractSandboxedUiAdapter
 import androidx.privacysandbox.ui.provider.toCoreLibInfo
 import androidx.privacysandbox.ui.tests.endtoend.IntegrationTests
@@ -60,10 +63,12 @@
         hasFailingTestSession: Boolean = false,
         placeViewInsideFrameLayout: Boolean = false,
         viewForSession: SandboxedSdkView?,
-        testSessionClient: TestSessionClient = TestSessionClient()
+        testSessionClient: TestSessionClient = TestSessionClient(),
+        sessionObserverFactories: List<SessionObserverFactory>? = null
     ): TestSandboxedUiAdapter {
 
         val adapter = TestSandboxedUiAdapter(hasFailingTestSession, placeViewInsideFrameLayout)
+        sessionObserverFactories?.forEach { adapter.addObserverFactory(it) }
         val adapterFromCoreLibInfo =
             SandboxedUiAdapterFactory.createFromCoreLibInfo(getCoreLibInfoFromAdapter(adapter))
         if (viewForSession != null) {
@@ -94,14 +99,16 @@
     fun createAdapterAndWaitToBeActive(
         initialZOrder: Boolean = true,
         viewForSession: SandboxedSdkView,
-        placeViewInsideFrameLayout: Boolean = false
+        placeViewInsideFrameLayout: Boolean = false,
+        sessionObserverFactories: List<SessionObserverFactory>? = null,
     ): TestSandboxedUiAdapter {
         viewForSession.orderProviderUiAboveClientUi(initialZOrder)
 
         val adapter =
             createAdapterAndEstablishSession(
                 placeViewInsideFrameLayout = placeViewInsideFrameLayout,
-                viewForSession = viewForSession
+                viewForSession = viewForSession,
+                sessionObserverFactories = sessionObserverFactories
             )
 
         val activeLatch = CountDownLatch(1)
@@ -166,8 +173,8 @@
          */
         inner class FailingTestSession(
             private val context: Context,
-            private val sessionClient: SandboxedUiAdapter.SessionClient
-        ) : SandboxedUiAdapter.Session {
+            sessionClient: SandboxedUiAdapter.SessionClient
+        ) : TestSession(context, sessionClient) {
             override val view: View
                 get() {
                     sessionClient.onSessionError(Throwable("Test Session Exception"))
@@ -183,7 +190,7 @@
             override fun close() {}
         }
 
-        inner class TestSession(
+        open inner class TestSession(
             private val context: Context,
             val sessionClient: SandboxedUiAdapter.SessionClient,
             private val placeViewInsideFrameLayout: Boolean = false
@@ -246,14 +253,13 @@
                     }
                 }
 
-            override val view: View
-                get() {
-                    return if (placeViewInsideFrameLayout) {
-                        FrameLayout(context).also { it.addView(testView) }
-                    } else {
-                        testView
-                    }
+            override val view: View by lazy {
+                if (placeViewInsideFrameLayout) {
+                    FrameLayout(context).also { it.addView(testView) }
+                } else {
+                    testView
                 }
+            }
 
             override fun notifyResized(width: Int, height: Int) {
                 resizedWidth = width
@@ -328,6 +334,54 @@
         }
     }
 
+    class SessionObserverFactoryImpl : SessionObserverFactory {
+        val sessionObservers: MutableList<SessionObserverImpl> = mutableListOf()
+        private val sessionObserverCreatedLatch = CountDownLatch(1)
+
+        override fun create(): SessionObserver {
+            sessionObserverCreatedLatch.countDown()
+            val sessionObserver = SessionObserverImpl()
+            sessionObservers.add(sessionObserver)
+            return sessionObserver
+        }
+
+        fun assertNoSessionsAreCreated() {
+            assertThat(sessionObserverCreatedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isFalse()
+        }
+    }
+
+    class SessionObserverImpl : SessionObserver {
+        var sessionObserverContext: SessionObserverContext? = null
+        private val sessionOpenedLatch = CountDownLatch(1)
+        private val sessionClosedLatch = CountDownLatch(1)
+        private val uiContainerChangedLatch = CountDownLatch(1)
+
+        override fun onSessionOpened(sessionObserverContext: SessionObserverContext) {
+            this.sessionObserverContext = sessionObserverContext
+            sessionOpenedLatch.countDown()
+        }
+
+        override fun onUiContainerChanged(uiContainerInfo: Bundle) {
+            uiContainerChangedLatch.countDown()
+        }
+
+        override fun onSessionClosed() {
+            sessionClosedLatch.countDown()
+        }
+
+        fun assertSessionOpened() {
+            assertThat(sessionOpenedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        }
+
+        fun assertOnUiContainerChangedSent() {
+            assertThat(uiContainerChangedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        }
+
+        fun assertSessionClosed() {
+            assertThat(sessionClosedLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+        }
+    }
+
     fun getCoreLibInfoFromAdapter(sdkAdapter: SandboxedUiAdapter): Bundle {
         val bundle = sdkAdapter.toCoreLibInfo(context)
         bundle.putBoolean(TEST_ONLY_USE_REMOTE_ADAPTER, !invokeBackwardsCompatFlow)
diff --git a/security/security-state/src/androidTest/AndroidManifest.xml b/security/security-state/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..4ce03fb
--- /dev/null
+++ b/security/security-state/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <application>
+    </application>
+
+</manifest>
diff --git a/security/security-state/src/androidTest/java/androidx/security/state/SecurityStateManagerTest.kt b/security/security-state/src/androidTest/java/androidx/security/state/SecurityStateManagerTest.kt
new file mode 100644
index 0000000..35e8762
--- /dev/null
+++ b/security/security-state/src/androidTest/java/androidx/security/state/SecurityStateManagerTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.security.state
+
+import android.content.Context
+import android.os.Build
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+class SecurityStateManagerTest {
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var securityStateManager: SecurityStateManager
+
+    @Before
+    fun setup() {
+        securityStateManager = SecurityStateManager(context)
+    }
+
+    @Test
+    fun testGetGlobalSecurityState() {
+        val bundle = securityStateManager.getGlobalSecurityState()
+
+        // Check if dates are in the format YYYY-MM-DD
+        val dateRegex = "^\\d{4}-\\d{2}-\\d{2}$"
+        assertTrue(bundle.getString("system_spl")!!.matches(dateRegex.toRegex()))
+        assertTrue(bundle.getString("vendor_spl")!!.matches(dateRegex.toRegex()))
+
+        // Check if kernel version is in the format X.X.XX
+        val versionRegex = "^\\d+\\.\\d+\\.\\d+$"
+        assertTrue(bundle.getString("kernel_version")!!.matches(versionRegex.toRegex()))
+    }
+}
diff --git a/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt b/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
index 341e497..4f8a68f 100644
--- a/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
+++ b/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
@@ -417,7 +417,7 @@
             )
         }
 
-        val completeUrl = "$serverUrl/v1/AndroidSDK$androidSdk.json"
+        val completeUrl = "$serverUrl/v1/android_sdk_$androidSdk.json"
 
         return Uri.parse(completeUrl)
     }
diff --git a/security/security-state/src/main/java/androidx/security/state/SecurityStateManager.kt b/security/security-state/src/main/java/androidx/security/state/SecurityStateManager.kt
index f388332..88e75d8 100644
--- a/security/security-state/src/main/java/androidx/security/state/SecurityStateManager.kt
+++ b/security/security-state/src/main/java/androidx/security/state/SecurityStateManager.kt
@@ -21,11 +21,11 @@
 import android.content.pm.PackageManager
 import android.os.Build
 import android.os.Bundle
+import android.system.Os
 import android.util.Log
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.webkit.WebViewCompat
-import java.io.File
 import java.util.regex.Pattern
 
 /**
@@ -195,7 +195,7 @@
     }
 
     /**
-     * Attempts to retrieve the kernel version of the device from the system's '/proc/version' file.
+     * Attempts to retrieve the kernel version of the device using the system's uname() command.
      * This method is used to determine the current kernel version which is a part of the security
      * state assessment.
      *
@@ -204,7 +204,7 @@
      */
     private fun getKernelVersion(): String {
         try {
-            val matcher = kernelReleasePattern.matcher(File("/proc/version").readText())
+            val matcher = kernelReleasePattern.matcher(Os.uname().release)
             return if (matcher.matches()) matcher.group(1)!! else ""
         } catch (e: Exception) {
             return ""
diff --git a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt b/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
index ae083f4..ba62223 100644
--- a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
+++ b/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
@@ -203,7 +203,7 @@
     fun testGetVulnerabilityReportUrl_validSdkVersion_returnsCorrectUrl() {
         val sdkVersion = 34 // Android 14
         val baseUrl = SecurityPatchState.DEFAULT_VULNERABILITY_REPORTS_URL
-        val expectedUrl = "$baseUrl/v1/AndroidSDK34.json"
+        val expectedUrl = "$baseUrl/v1/android_sdk_34.json"
 
         doReturn(sdkVersion).`when`(mockSecurityStateManager).getAndroidSdkInt()
 
diff --git a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateManagerTest.kt b/security/security-state/src/test/java/androidx/security/state/SecurityStateManagerTest.kt
similarity index 98%
rename from security/security-state/src/test/java/androidx/security/state/SecurityPatchStateManagerTest.kt
rename to security/security-state/src/test/java/androidx/security/state/SecurityStateManagerTest.kt
index 9df96fa..19c2f1d 100644
--- a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateManagerTest.kt
+++ b/security/security-state/src/test/java/androidx/security/state/SecurityStateManagerTest.kt
@@ -34,7 +34,7 @@
 import org.robolectric.annotation.Config
 
 @RunWith(JUnit4::class)
-class SecurityPatchStateManagerTest {
+class SecurityStateManagerTest {
 
     private val packageManager: PackageManager = mock<PackageManager>()
     private val context: Context = mock<Context>() { on { packageManager } doReturn packageManager }
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index c98f15b..78025bf 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -32,7 +32,7 @@
 }
 
 dependencies {
-    api project(':compose:foundation:foundation')
+    api("androidx.compose.foundation:foundation:1.6.0")
     api("androidx.compose.ui:ui:1.6.0")
     api("androidx.compose.ui:ui-text:1.6.0")
     api("androidx.compose.runtime:runtime:1.6.0")
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt
index 0057e26..b25dc9d 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/ScalingLazyColumnTest.kt
@@ -26,7 +26,6 @@
 import androidx.compose.foundation.layout.requiredWidth
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.getValue
@@ -35,13 +34,11 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.testutils.WithTouchSlop
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertHeightIsEqualTo
 import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsFocused
 import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -945,58 +942,6 @@
             }
         }
     }
-
-    @Test
-    fun scalingLazyColumnIsFocusedByDefault() {
-        var focusSet = false
-
-        rule.setContent {
-            ScalingLazyColumn(
-                modifier =
-                    Modifier.testTag("scalingLazyColumn").onFocusChanged {
-                        focusSet = it.isFocused
-                    },
-            ) {
-                items(100) { BasicText("item $it") }
-            }
-        }
-
-        assert(focusSet)
-    }
-
-    @Test
-    fun scalingLazyColumnIsNotFocused_withDisabledRotary() {
-        var focusSet = false
-
-        rule.setContent {
-            ScalingLazyColumn(
-                modifier =
-                    Modifier.testTag("scalingLazyColumn").onFocusChanged {
-                        focusSet = it.isFocused
-                    },
-                // Disable rotary and focus as well
-                rotaryScrollableBehavior = null,
-            ) {
-                items(100) { BasicText("item $it") }
-            }
-        }
-
-        assert(!focusSet)
-    }
-
-    @Test
-    fun scalingLazyColumnIsFocusedByDefault_withSemantics() {
-
-        rule.setContent {
-            ScalingLazyColumn(
-                modifier = Modifier.testTag("scalingLazyColumn"),
-            ) {
-                items(100) { BasicText("item $it") }
-            }
-        }
-
-        rule.onNodeWithTag("scalingLazyColumn").assertIsFocused()
-    }
 }
 
 internal const val TestTouchSlop = 18f
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/FocusTargetWithSemantics.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/FocusTargetWithSemantics.kt
deleted file mode 100644
index 4b22c73..0000000
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/FocusTargetWithSemantics.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.compose.foundation.rotary
-
-import androidx.compose.runtime.Stable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusState
-import androidx.compose.ui.focus.FocusTargetModifierNode
-import androidx.compose.ui.node.DelegatingNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.node.SemanticsModifierNode
-import androidx.compose.ui.node.invalidateSemantics
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.semantics.SemanticsPropertyReceiver
-import androidx.compose.ui.semantics.focused
-import androidx.compose.ui.semantics.requestFocus
-
-/**
- * FocusTarget modifier with Semantics node. Uses underlying focusTarget node with additional
- * semantics node.
- */
-@Stable internal fun Modifier.focusTargetWithSemantics() = this then FocusTargetWithSemanticsElement
-
-private object FocusTargetWithSemanticsElement :
-    ModifierNodeElement<FocusTargetWithSemanticsNode>() {
-
-    override fun create() = FocusTargetWithSemanticsNode()
-
-    override fun update(node: FocusTargetWithSemanticsNode) {}
-
-    override fun InspectorInfo.inspectableProperties() {
-        name = "focusTargetWithSemanticsElement"
-    }
-
-    override fun hashCode() = "focusTargetWithSemanticsElement".hashCode()
-
-    override fun equals(other: Any?) = other === this
-}
-
-internal class FocusTargetWithSemanticsNode() : DelegatingNode(), SemanticsModifierNode {
-
-    private val focusTargetNode =
-        delegate(FocusTargetModifierNode(onFocusChange = ::onFocusStateChange))
-
-    private var requestFocus: (() -> Boolean)? = null
-
-    private fun onFocusStateChange(previousState: FocusState, currentState: FocusState) {
-        if (!isAttached) return
-        if (currentState.isFocused != previousState.isFocused) invalidateSemantics()
-    }
-
-    override fun SemanticsPropertyReceiver.applySemantics() {
-        focused = focusTargetNode.focusState.isFocused
-        if (requestFocus == null) {
-            requestFocus = { focusTargetNode.requestFocus() }
-        }
-        requestFocus(action = requestFocus)
-    }
-}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt
index ada6885..98e459b 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/rotary/RotaryScrollable.kt
@@ -35,6 +35,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.focusTarget
 import androidx.compose.ui.input.rotary.RotaryInputModifierNode
 import androidx.compose.ui.input.rotary.RotaryScrollEvent
 import androidx.compose.ui.node.ModifierNodeElement
@@ -106,7 +107,7 @@
             reverseDirection = reverseDirection,
         )
         .focusRequester(focusRequester)
-        .focusTargetWithSemantics()
+        .focusTarget()
 
 /**
  * An interface for handling scroll events. Has implementations for handling scroll with/without
diff --git a/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt
new file mode 100644
index 0000000..83ed677
--- /dev/null
+++ b/wear/compose/compose-material/benchmark/src/androidTest/java/androidx/wear/compose/material/benchmark/ScalingLazyColumnBenchmark.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material.benchmark
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkDrawPerf
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkLayoutPerf
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.ScalingLazyColumn
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.rememberScalingLazyListState
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Benchmark for Wear Compose ScalingLazyColumn. */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class ScalingLazyColumnBenchmark {
+
+    @get:Rule val benchmarkRule = ComposeBenchmarkRule()
+
+    private val scalingLazyColumnCaseFactory = { ScalingLazyColumnTestCase() }
+
+    @Test
+    fun first_compose() {
+        benchmarkRule.benchmarkFirstCompose(scalingLazyColumnCaseFactory)
+    }
+
+    @Test
+    fun first_measure() {
+        benchmarkRule.benchmarkFirstMeasure(scalingLazyColumnCaseFactory)
+    }
+
+    @Test
+    fun first_layout() {
+        benchmarkRule.benchmarkFirstLayout(scalingLazyColumnCaseFactory)
+    }
+
+    @Test
+    fun first_draw() {
+        benchmarkRule.benchmarkFirstDraw(scalingLazyColumnCaseFactory)
+    }
+
+    @Test
+    fun layout() {
+        benchmarkRule.benchmarkLayoutPerf(scalingLazyColumnCaseFactory)
+    }
+
+    @Test
+    fun draw() {
+        benchmarkRule.benchmarkDrawPerf(scalingLazyColumnCaseFactory)
+    }
+}
+
+@Suppress("DEPRECATION")
+internal class ScalingLazyColumnTestCase : LayeredComposeTestCase() {
+    private var itemSizeDp: Dp = 10.dp
+    private var defaultItemSpacingDp: Dp = 4.dp
+
+    @Composable
+    override fun MeasuredContent() {
+        ScalingLazyColumn(
+            state = rememberScalingLazyListState(),
+            modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
+        ) {
+            items(10) { it -> Box(Modifier.requiredSize(itemSizeDp)) { Text(text = "Item $it") } }
+        }
+    }
+
+    @Composable
+    override fun ContentWrappers(content: @Composable () -> Unit) {
+        MaterialTheme { content() }
+    }
+}
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
index af527de..bbe70aa 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
@@ -30,8 +30,6 @@
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.calculateEndPadding
-import androidx.compose.foundation.layout.calculateStartPadding
 import androidx.compose.foundation.layout.defaultMinSize
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.height
@@ -52,7 +50,6 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.takeOrElse
-import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.role
@@ -60,6 +57,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.wear.compose.material3.tokens.MotionTokens
 import androidx.wear.compose.material3.tokens.RadioButtonTokens
+import androidx.wear.compose.material3.tokens.ShapeTokens
 import androidx.wear.compose.material3.tokens.SplitRadioButtonTokens
 import androidx.wear.compose.materialcore.animateSelectionColor
 
@@ -263,7 +261,7 @@
     secondaryLabel: @Composable (RowScope.() -> Unit)? = null,
     label: @Composable RowScope.() -> Unit
 ) {
-    val (startPadding, endPadding) = contentPadding.splitHorizontally()
+    val containerColor = colors.containerColor(enabled, selected).value
 
     Row(
         verticalAlignment = Alignment.CenterVertically,
@@ -273,7 +271,6 @@
                 .height(IntrinsicSize.Min)
                 .width(IntrinsicSize.Max)
                 .clip(shape = shape)
-                .background(colors.containerColor(enabled, selected).value)
     ) {
         Row(
             modifier =
@@ -286,7 +283,9 @@
                     )
                     .semantics { role = Role.Button }
                     .fillMaxHeight()
-                    .then(startPadding)
+                    .clip(SPLIT_SECTIONS_SHAPE)
+                    .background(containerColor)
+                    .padding(contentPadding)
                     .weight(1.0f),
             verticalAlignment = Alignment.CenterVertically,
         ) {
@@ -305,9 +304,10 @@
                         content = secondaryLabel
                     ),
             )
-            Spacer(modifier = Modifier.size(SELECTION_CONTROL_SPACING))
         }
 
+        Spacer(modifier = Modifier.size(2.dp))
+
         val splitContainerColor =
             colors.splitContainerColor(enabled = enabled, selected = selected).value
         Box(
@@ -320,6 +320,8 @@
                         interactionSource = selectionInteractionSource
                     )
                     .fillMaxHeight()
+                    .clip(SPLIT_SECTIONS_SHAPE)
+                    .background(containerColor)
                     .drawWithCache {
                         onDrawWithContent {
                             drawRect(color = splitContainerColor)
@@ -329,8 +331,7 @@
                     .align(Alignment.CenterVertically)
                     .width(SPLIT_WIDTH)
                     .wrapContentHeight(align = Alignment.CenterVertically)
-                    .wrapContentWidth(align = Alignment.End)
-                    .then(endPadding)
+                    .padding(contentPadding)
                     .semantics {
                         // For a selectable button, the role is always RadioButton.
                         // See also b/330869742 for issue with setting the SelectableButton role
@@ -1334,21 +1335,6 @@
     }
 }
 
-@Composable
-private fun PaddingValues.splitHorizontally() =
-    Modifier.padding(
-        start = calculateStartPadding(LocalLayoutDirection.current),
-        end = 0.dp,
-        top = calculateTopPadding(),
-        bottom = calculateBottomPadding()
-    ) to
-        Modifier.padding(
-            start = 0.dp,
-            end = calculateEndPadding(layoutDirection = LocalLayoutDirection.current),
-            top = calculateTopPadding(),
-            bottom = calculateBottomPadding()
-        )
-
 private val COLOR_ANIMATION_SPEC: AnimationSpec<Color> =
     tween(MotionTokens.DurationMedium1, 0, MotionTokens.EasingStandardDecelerate)
 private val SELECTION_CONTROL_WIDTH = 32.dp
@@ -1357,5 +1343,6 @@
 private val ICON_SPACING = 6.dp
 private val MIN_HEIGHT = 52.dp
 private val SPLIT_WIDTH = 52.dp
-private val CONTROL_WIDTH = 32.dp
+private val CONTROL_WIDTH = 24.dp
 private val CONTROL_HEIGHT = 24.dp
+private val SPLIT_SECTIONS_SHAPE = ShapeTokens.CornerExtraSmall
diff --git a/wear/protolayout/protolayout-material-core/build.gradle b/wear/protolayout/protolayout-material-core/build.gradle
index ca1740a..dcf081a 100644
--- a/wear/protolayout/protolayout-material-core/build.gradle
+++ b/wear/protolayout/protolayout-material-core/build.gradle
@@ -26,7 +26,6 @@
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
-    id("kotlin-android")
 }
 
 android {
@@ -56,7 +55,6 @@
     testImplementation(libs.testRunner)
     testImplementation(libs.testRules)
     testImplementation(libs.truth)
-    testImplementation("androidx.core:core-ktx:1.13.1")
 }
 
 androidx {
diff --git a/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverter.java b/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverter.java
deleted file mode 100644
index 98351ea..0000000
--- a/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverter.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR 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.materialcore.fontscaling;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-
-import java.util.Arrays;
-
-/**
- * A lookup table for non-linear font scaling. Converts font sizes given in "sp" dimensions to a
- * "dp" dimension according to a non-linear curve.
- *
- * <p>This is meant to improve readability at larger font scales: larger fonts will scale up more
- * slowly than smaller fonts, so we don't get ridiculously huge fonts that don't fit on the screen.
- *
- * <p>The thinking here is that large fonts are already big enough to read, but we still want to
- * scale them slightly to preserve the visual hierarchy when compared to smaller fonts.
- */
-// This is copied from
-// https://cs.android.com/android/_/android/platform/frameworks/base/+/2a4e99a798cc69944f64d54b81aee987fbea45d6:core/java/android/content/res/FontScaleConverter.java
-// TODO: b/342359552 - Use Android Platform api instead when it becomes public.
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class FontScaleConverter {
-
-    final float[] mFromSpValues;
-    final float[] mToDpValues;
-
-    /**
-     * Creates a lookup table for the given conversions.
-     *
-     * <p>Any "sp" value not in the lookup table will be derived via linear interpolation.
-     *
-     * <p>The arrays must be sorted ascending and monotonically increasing.
-     *
-     * @param fromSp array of dimensions in SP
-     * @param toDp array of dimensions in DP that correspond to an SP value in fromSp
-     * @throws IllegalArgumentException if the array lengths don't match or are empty
-     */
-    FontScaleConverter(@NonNull float[] fromSp, @NonNull float[] toDp) {
-        if (fromSp.length != toDp.length || fromSp.length == 0) {
-            throw new IllegalArgumentException("Array lengths must match and be nonzero");
-        }
-
-        mFromSpValues = fromSp;
-        mToDpValues = toDp;
-    }
-
-    /** Convert a dimension in "dp" back to "sp" using the lookup table. */
-    public float convertDpToSp(float dp) {
-        return lookupAndInterpolate(dp, mToDpValues, mFromSpValues);
-    }
-
-    /** Convert a dimension in "sp" to "dp" using the lookup table. */
-    public float convertSpToDp(float sp) {
-        return lookupAndInterpolate(sp, mFromSpValues, mToDpValues);
-    }
-
-    private static float lookupAndInterpolate(
-            float sourceValue, float[] sourceValues, float[] targetValues) {
-        final float sourceValuePositive = Math.abs(sourceValue);
-        // TODO(b/247861374): find a match at a higher index?
-        final float sign = Math.signum(sourceValue);
-        // We search for exact matches only, even if it's just a little off. The interpolation will
-        // handle any non-exact matches.
-        final int index = Arrays.binarySearch(sourceValues, sourceValuePositive);
-        if (index >= 0) {
-            // exact match, return the matching dp
-            return sign * targetValues[index];
-        } else {
-            // must be a value in between index and index + 1: interpolate.
-            final int lowerIndex = -(index + 1) - 1;
-
-            final float startSp;
-            final float endSp;
-            final float startDp;
-            final float endDp;
-
-            if (lowerIndex >= sourceValues.length - 1) {
-                // It's past our lookup table. Determine the last elements' scaling factor and use.
-                startSp = sourceValues[sourceValues.length - 1];
-                startDp = targetValues[sourceValues.length - 1];
-
-                if (startSp == 0) {
-                    return 0;
-                }
-
-                final float scalingFactor = startDp / startSp;
-                return sourceValue * scalingFactor;
-            } else if (lowerIndex == -1) {
-                // It's smaller than the smallest value in our table. Interpolate from 0.
-                startSp = 0;
-                startDp = 0;
-                endSp = sourceValues[0];
-                endDp = targetValues[0];
-            } else {
-                startSp = sourceValues[lowerIndex];
-                endSp = sourceValues[lowerIndex + 1];
-                startDp = targetValues[lowerIndex];
-                endDp = targetValues[lowerIndex + 1];
-            }
-
-            return sign
-                    * MathUtils.constrainedMap(startDp, endDp, startSp, endSp, sourceValuePositive);
-        }
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null) {
-            return false;
-        }
-        if (!(o instanceof FontScaleConverter)) {
-            return false;
-        }
-        FontScaleConverter that = (FontScaleConverter) o;
-        return Arrays.equals(mFromSpValues, that.mFromSpValues)
-                && Arrays.equals(mToDpValues, that.mToDpValues);
-    }
-
-    @Override
-    public int hashCode() {
-        int result = Arrays.hashCode(mFromSpValues);
-        result = 31 * result + Arrays.hashCode(mToDpValues);
-        return result;
-    }
-
-    @NonNull
-    @Override
-    public String toString() {
-        return "FontScaleConverter{"
-                + "fromSpValues="
-                + Arrays.toString(mFromSpValues)
-                + ", toDpValues="
-                + Arrays.toString(mToDpValues)
-                + '}';
-    }
-}
diff --git a/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterFactory.java b/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterFactory.java
deleted file mode 100644
index 5b8b3f7..0000000
--- a/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterFactory.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR 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.materialcore.fontscaling;
-
-import android.content.res.Configuration;
-import android.util.SparseArray;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
-
-/** Stores lookup tables for creating {@link FontScaleConverter}s at various scales. */
-// This is copied from
-// https://cs.android.com/android/_/android/platform/frameworks/base/+/2a4e99a798cc69944f64d54b81aee987fbea45d6:core/java/android/content/res/FontScaleConverterFactory.java
-// TODO: b/342359552 - Use Android Platform api instead when it becomes public.
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class FontScaleConverterFactory {
-    private static final float SCALE_KEY_MULTIPLIER = 100f;
-
-    @VisibleForTesting
-    static final SparseArray<FontScaleConverter> LOOKUP_TABLES = new SparseArray<>();
-
-    @SuppressWarnings("NonFinalStaticField")
-    private static float sMinScaleBeforeCurvesApplied = 1.05f;
-
-    static {
-        // These were generated by frameworks/base/tools/fonts/font-scaling-array-generator.js and
-        // manually tweaked for optimum readability.
-        put(
-                /* scaleKey= */ 1.15f,
-                new FontScaleConverter(
-                        /* fromSp= */ new float[] {8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
-                        /* toDp= */ new float[] {
-                            9.2f, 11.5f, 13.8f, 16.4f, 19.8f, 21.8f, 25.2f, 30f, 100
-                        }));
-
-        put(
-                /* scaleKey= */ 1.3f,
-                new FontScaleConverter(
-                        /* fromSp= */ new float[] {8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
-                        /* toDp= */ new float[] {
-                            10.4f, 13f, 15.6f, 18.8f, 21.6f, 23.6f, 26.4f, 30f, 100
-                        }));
-
-        put(
-                /* scaleKey= */ 1.5f,
-                new FontScaleConverter(
-                        /* fromSp= */ new float[] {8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
-                        /* toDp= */ new float[] {12f, 15f, 18f, 22f, 24f, 26f, 28f, 30f, 100}));
-
-        put(
-                /* scaleKey= */ 1.8f,
-                new FontScaleConverter(
-                        /* fromSp= */ new float[] {8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
-                        /* toDp= */ new float[] {
-                            14.4f, 18f, 21.6f, 24.4f, 27.6f, 30.8f, 32.8f, 34.8f, 100
-                        }));
-
-        put(
-                /* scaleKey= */ 2f,
-                new FontScaleConverter(
-                        /* fromSp= */ new float[] {8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
-                        /* toDp= */ new float[] {16f, 20f, 24f, 26f, 30f, 34f, 36f, 38f, 100}));
-
-        sMinScaleBeforeCurvesApplied = getScaleFromKey(LOOKUP_TABLES.keyAt(0)) - 0.02f;
-        if (sMinScaleBeforeCurvesApplied <= 1.0f) {
-            throw new IllegalStateException(
-                    "You should only apply non-linear scaling to font scales > 1");
-        }
-    }
-
-    private FontScaleConverterFactory() {}
-
-    /**
-     * Returns true if non-linear font scaling curves would be in effect for the given scale, false
-     * if the scaling would follow a linear curve or for no scaling.
-     *
-     * <p>Example usage: <code>
-     * isNonLinearFontScalingActive(getResources().getConfiguration().fontScale)</code>
-     */
-    public static boolean isNonLinearFontScalingActive(float fontScale) {
-        return fontScale >= sMinScaleBeforeCurvesApplied;
-    }
-
-    /**
-     * Finds a matching FontScaleConverter for the given fontScale factor.
-     *
-     * @param fontScale the scale factor, usually from {@link Configuration#fontScale}.
-     * @return a converter for the given scale, or null if non-linear scaling should not be used.
-     */
-    @Nullable
-    public static FontScaleConverter forScale(float fontScale) {
-        if (!isNonLinearFontScalingActive(fontScale)) {
-            return null;
-        }
-
-        FontScaleConverter lookupTable = get(fontScale);
-        if (lookupTable != null) {
-            return lookupTable;
-        }
-
-        // Didn't find an exact match: interpolate between two existing tables
-        final int index = LOOKUP_TABLES.indexOfKey(getKey(fontScale));
-        if (index >= 0) {
-            // This should never happen, should have been covered by get() above.
-            return LOOKUP_TABLES.valueAt(index);
-        }
-        // Didn't find an exact match: interpolate between two existing tables
-        final int lowerIndex = -(index + 1) - 1;
-        final int higherIndex = lowerIndex + 1;
-        if (lowerIndex < 0 || higherIndex >= LOOKUP_TABLES.size()) {
-            // We have gone beyond our bounds and have nothing to interpolate between. Just give
-            // them a straight linear table instead.
-            // This works because when FontScaleConverter encounters a size beyond its bounds, it
-            // calculates a linear fontScale factor using the ratio of the last element pair.
-            return new FontScaleConverter(new float[] {1f}, new float[] {fontScale});
-        } else {
-            float startScale = getScaleFromKey(LOOKUP_TABLES.keyAt(lowerIndex));
-            float endScale = getScaleFromKey(LOOKUP_TABLES.keyAt(higherIndex));
-            float interpolationPoint =
-                    MathUtils.constrainedMap(
-                            /* rangeMin= */ 0f,
-                            /* rangeMax= */ 1f,
-                            startScale,
-                            endScale,
-                            fontScale);
-            return createInterpolatedTableBetween(
-                    LOOKUP_TABLES.valueAt(lowerIndex),
-                    LOOKUP_TABLES.valueAt(higherIndex),
-                    interpolationPoint);
-        }
-    }
-
-    @NonNull
-    private static FontScaleConverter createInterpolatedTableBetween(
-            FontScaleConverter start, FontScaleConverter end, float interpolationPoint) {
-        float[] commonSpSizes = new float[] {8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100f};
-        float[] dpInterpolated = new float[commonSpSizes.length];
-
-        for (int i = 0; i < commonSpSizes.length; i++) {
-            float sp = commonSpSizes[i];
-            float startDp = start.convertSpToDp(sp);
-            float endDp = end.convertSpToDp(sp);
-            dpInterpolated[i] = MathUtils.lerp(startDp, endDp, interpolationPoint);
-        }
-
-        return new FontScaleConverter(commonSpSizes, dpInterpolated);
-    }
-
-    private static int getKey(float fontScale) {
-        return (int) (fontScale * SCALE_KEY_MULTIPLIER);
-    }
-
-    private static float getScaleFromKey(int key) {
-        return (float) key / SCALE_KEY_MULTIPLIER;
-    }
-
-    private static void put(float scaleKey, @NonNull FontScaleConverter fontScaleConverter) {
-        LOOKUP_TABLES.put(getKey(scaleKey), fontScaleConverter);
-    }
-
-    @Nullable
-    private static FontScaleConverter get(float scaleKey) {
-        return LOOKUP_TABLES.get(getKey(scaleKey));
-    }
-}
diff --git a/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/MathUtils.java b/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/MathUtils.java
deleted file mode 100644
index 5773d03..0000000
--- a/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/fontscaling/MathUtils.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR 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.materialcore.fontscaling;
-
-import static java.lang.Math.min;
-
-/** A class that contains utility methods related to numbers. */
-final class MathUtils {
-    private MathUtils() {}
-
-    /**
-     * Returns the linear interpolation of {@code amount} between {@code start} and {@code stop}.
-     */
-    static float lerp(float start, float stop, float amount) {
-        return start + (stop - start) * amount;
-    }
-
-    /**
-     * Returns the interpolation scalar (s) that satisfies the equation: {@code value = }{@link
-     * #lerp}{@code (a, b, s)}
-     *
-     * <p>If {@code a == b}, then this function will return 0.
-     */
-    static float lerpInv(float a, float b, float value) {
-        return a != b ? ((value - a) / (b - a)) : 0.0f;
-    }
-
-    /** Returns the single argument constrained between [0.0, 1.0]. */
-    static float saturate(float value) {
-        return value < 0.0f ? 0.0f : min(1.0f, value);
-    }
-
-    /** Returns the saturated (constrained between [0, 1]) result of {@link #lerpInv}. */
-    static float lerpInvSat(float a, float b, float value) {
-        return saturate(lerpInv(a, b, value));
-    }
-
-    /**
-     * Calculates a value in [rangeMin, rangeMax] that maps value in [valueMin, valueMax] to
-     * returnVal in [rangeMin, rangeMax].
-     *
-     * <p>Always returns a constrained value in the range [rangeMin, rangeMax], even if value is
-     * outside [valueMin, valueMax].
-     *
-     * <p>Eg: constrainedMap(0f, 100f, 0f, 1f, 0.5f) = 50f constrainedMap(20f, 200f, 10f, 20f, 20f)
-     * = 200f constrainedMap(20f, 200f, 10f, 20f, 50f) = 200f constrainedMap(10f, 50f, 10f, 20f, 5f)
-     * = 10f
-     *
-     * @param rangeMin minimum of the range that should be returned.
-     * @param rangeMax maximum of the range that should be returned.
-     * @param valueMin minimum of range to map {@code value} to.
-     * @param valueMax maximum of range to map {@code value} to.
-     * @param value to map to the range [{@code valueMin}, {@code valueMax}]. Note, can be outside
-     *     this range, resulting in a clamped value.
-     * @return the mapped value, constrained to [{@code rangeMin}, {@code rangeMax}.
-     */
-    static float constrainedMap(
-            float rangeMin, float rangeMax, float valueMin, float valueMax, float value) {
-        return lerp(rangeMin, rangeMax, lerpInvSat(valueMin, valueMax, value));
-    }
-}
diff --git a/wear/protolayout/protolayout-material-core/src/test/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterFactoryTest.kt b/wear/protolayout/protolayout-material-core/src/test/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterFactoryTest.kt
deleted file mode 100644
index 841825b..0000000
--- a/wear/protolayout/protolayout-material-core/src/test/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterFactoryTest.kt
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR 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.materialcore.fontscaling
-
-import androidx.core.util.forEach
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import kotlin.math.ceil
-import kotlin.math.floor
-import kotlin.random.Random.Default.nextFloat
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class FontScaleConverterFactoryTest {
-
-    @Test
-    fun scale200IsTwiceAtSmallSizes() {
-        val table = FontScaleConverterFactory.forScale(2F)!!
-        assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(2f)
-        assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(16f)
-        assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(20f)
-        assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(10f)
-        assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
-    }
-
-    @LargeTest
-    @Test
-    fun missingLookupTablePastEnd_returnsLinear() {
-        val table = FontScaleConverterFactory.forScale(3F)!!
-        generateSequenceOfFractions(-10000f..10000f, step = 0.01f).map {
-            assertThat(table.convertSpToDp(it)).isWithin(CONVERSION_TOLERANCE).of(it * 3f)
-        }
-        assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(3f)
-        assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(24f)
-        assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(30f)
-        assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(15f)
-        assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
-        assertThat(table.convertSpToDp(50F)).isWithin(CONVERSION_TOLERANCE).of(150f)
-        assertThat(table.convertSpToDp(100F)).isWithin(CONVERSION_TOLERANCE).of(300f)
-    }
-
-    @SmallTest
-    fun missingLookupTable110_returnsInterpolated() {
-        val table = FontScaleConverterFactory.forScale(1.1F)!!
-
-        assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(1.1f)
-        assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(8f * 1.1f)
-        assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(11f)
-        assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(5f * 1.1f)
-        assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
-        assertThat(table.convertSpToDp(50F)).isLessThan(50f * 1.1f)
-        assertThat(table.convertSpToDp(100F)).isLessThan(100f * 1.1f)
-    }
-
-    @Test
-    fun missingLookupTable199_returnsInterpolated() {
-        val table = FontScaleConverterFactory.forScale(1.9999F)!!
-        assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(2f)
-        assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(16f)
-        assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(20f)
-        assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(10f)
-        assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
-    }
-
-    @Test
-    fun missingLookupTable160_returnsInterpolated() {
-        val table = FontScaleConverterFactory.forScale(1.6F)!!
-        assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(1f * 1.6F)
-        assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(8f * 1.6F)
-        assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(10f * 1.6F)
-        assertThat(table.convertSpToDp(20F)).isLessThan(20f * 1.6F)
-        assertThat(table.convertSpToDp(100F)).isLessThan(100f * 1.6F)
-        assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(5f * 1.6F)
-        assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
-    }
-
-    @SmallTest
-    fun missingLookupTableNegativeReturnsNull() {
-        assertThat(FontScaleConverterFactory.forScale(-1F)).isNull()
-    }
-
-    @SmallTest
-    fun unnecessaryFontScalesReturnsNull() {
-        assertThat(FontScaleConverterFactory.forScale(0F)).isNull()
-        assertThat(FontScaleConverterFactory.forScale(1F)).isNull()
-        assertThat(FontScaleConverterFactory.forScale(0.85F)).isNull()
-    }
-
-    @SmallTest
-    fun tablesMatchAndAreMonotonicallyIncreasing() {
-        FontScaleConverterFactory.LOOKUP_TABLES.forEach { _, lookupTable ->
-            assertThat(lookupTable.mToDpValues).hasLength(lookupTable.mFromSpValues.size)
-            assertThat(lookupTable.mToDpValues).isNotEmpty()
-
-            assertThat(lookupTable.mFromSpValues.asList()).isInStrictOrder()
-            assertThat(lookupTable.mToDpValues.asList()).isInStrictOrder()
-
-            assertThat(lookupTable.mFromSpValues.asList()).containsNoDuplicates()
-            assertThat(lookupTable.mToDpValues.asList()).containsNoDuplicates()
-        }
-    }
-
-    @SmallTest
-    fun testIsNonLinearFontScalingActive() {
-        assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1f)).isFalse()
-        assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(0f)).isFalse()
-        assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(-1f)).isFalse()
-        assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(0.85f)).isFalse()
-        assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.02f)).isFalse()
-        assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.10f)).isFalse()
-        assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.15f)).isTrue()
-        assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.1499999f)).isTrue()
-        assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.5f)).isTrue()
-        assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(2f)).isTrue()
-        assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(3f)).isTrue()
-    }
-
-    @LargeTest
-    @Test
-    fun allFeasibleScalesAndConversionsDoNotCrash() {
-        generateSequenceOfFractions(-10f..10f, step = 0.1f)
-            .fuzzFractions()
-            .mapNotNull { FontScaleConverterFactory.forScale(it) }
-            .flatMap { table ->
-                generateSequenceOfFractions(-2000f..2000f, step = 0.1f).fuzzFractions().map {
-                    Pair(table, it)
-                }
-            }
-            .forEach { (table, sp) ->
-                try {
-                    // Truth is slow because it creates a bunch of
-                    // objects. Don't use it unless we need to.
-                    if (!table.convertSpToDp(sp).isFinite()) {
-                        assertWithMessage("convertSpToDp(%s) on table: %s", sp, table)
-                            .that(table.convertSpToDp(sp))
-                            .isFinite()
-                    }
-                } catch (e: Exception) {
-                    throw AssertionError("Exception during convertSpToDp($sp) on table: $table", e)
-                }
-            }
-    }
-
-    @Test
-    fun testGenerateSequenceOfFractions() {
-        val fractions = generateSequenceOfFractions(-1000f..1000f, step = 0.1f).toList()
-        fractions.forEach {
-            assertThat(it).isAtLeast(-1000f)
-            assertThat(it).isAtMost(1000f)
-        }
-
-        assertThat(fractions).isInStrictOrder()
-        assertThat(fractions).hasSize(1000 * 2 * 10 + 1) // Don't forget the 0 in the middle!
-
-        assertThat(fractions).contains(100f)
-        assertThat(fractions).contains(500.1f)
-        assertThat(fractions).contains(500.2f)
-        assertThat(fractions).contains(0.2f)
-        assertThat(fractions).contains(0f)
-        assertThat(fractions).contains(-10f)
-        assertThat(fractions).contains(-10f)
-        assertThat(fractions).contains(-10.3f)
-
-        assertThat(fractions).doesNotContain(-10.31f)
-        assertThat(fractions).doesNotContain(0.35f)
-        assertThat(fractions).doesNotContain(0.31f)
-        assertThat(fractions).doesNotContain(-.35f)
-    }
-
-    @Test
-    fun testFuzzFractions() {
-        val numFuzzedFractions = 6
-        val fractions =
-            generateSequenceOfFractions(-1000f..1000f, step = 0.1f).fuzzFractions().toList()
-        fractions.forEach {
-            assertThat(it).isAtLeast(-1000f)
-            assertThat(it).isLessThan(1001f)
-        }
-
-        val numGeneratedFractions = 1000 * 2 * 10 + 1 // Don't forget the 0 in the middle!
-        assertThat(fractions).hasSize(numGeneratedFractions * numFuzzedFractions)
-
-        assertThat(fractions).contains(100f)
-        assertThat(fractions).contains(500.1f)
-        assertThat(fractions).contains(500.2f)
-        assertThat(fractions).contains(0.2f)
-        assertThat(fractions).contains(0f)
-        assertThat(fractions).contains(-10f)
-        assertThat(fractions).contains(-10f)
-        assertThat(fractions).contains(-10.3f)
-    }
-
-    companion object {
-        private const val CONVERSION_TOLERANCE = 0.05f
-    }
-}
-
-fun generateSequenceOfFractions(
-    range: ClosedFloatingPointRange<Float>,
-    step: Float
-): Sequence<Float> {
-    val multiplier = 1f / step
-    val start = floor(range.start * multiplier).toInt()
-    val endInclusive = ceil(range.endInclusive * multiplier).toInt()
-    return generateSequence(start) { it + 1 }
-        .takeWhile { it <= endInclusive }
-        .map { it.toFloat() / multiplier }
-}
-
-private fun Sequence<Float>.fuzzFractions(): Sequence<Float> {
-    return flatMap { i ->
-        listOf(i, i + 0.01f, i + 0.054f, i + 0.099f, i + nextFloat(), i + nextFloat())
-    }
-}
diff --git a/wear/protolayout/protolayout-material-core/src/test/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterTest.kt b/wear/protolayout/protolayout-material-core/src/test/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterTest.kt
deleted file mode 100644
index 7dc342a..0000000
--- a/wear/protolayout/protolayout-material-core/src/test/java/androidx/wear/protolayout/materialcore/fontscaling/FontScaleConverterTest.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR 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.materialcore.fontscaling
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertWithMessage
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class FontScaleConverterTest {
-
-    @Test
-    fun straightInterpolation() {
-        val table = createTable(8f to 8f, 10f to 10f, 20f to 20f)
-        verifyConversionBothWays(table, 1f, 1F)
-        verifyConversionBothWays(table, 8f, 8F)
-        verifyConversionBothWays(table, 10f, 10F)
-        verifyConversionBothWays(table, 30f, 30F)
-        verifyConversionBothWays(table, 20f, 20F)
-        verifyConversionBothWays(table, 5f, 5F)
-        verifyConversionBothWays(table, 0f, 0F)
-    }
-
-    @Test
-    fun interpolate200Percent() {
-        val table = createTable(8f to 16f, 10f to 20f, 30f to 60f)
-        verifyConversionBothWays(table, 2f, 1F)
-        verifyConversionBothWays(table, 16f, 8F)
-        verifyConversionBothWays(table, 20f, 10F)
-        verifyConversionBothWays(table, 60f, 30F)
-        verifyConversionBothWays(table, 40f, 20F)
-        verifyConversionBothWays(table, 10f, 5F)
-        verifyConversionBothWays(table, 0f, 0F)
-    }
-
-    @Test
-    fun interpolate150Percent() {
-        val table = createTable(2f to 3f, 10f to 15f, 20f to 30f, 100f to 150f)
-        verifyConversionBothWays(table, 3f, 2F)
-        verifyConversionBothWays(table, 1.5f, 1F)
-        verifyConversionBothWays(table, 12f, 8F)
-        verifyConversionBothWays(table, 15f, 10F)
-        verifyConversionBothWays(table, 30f, 20F)
-        verifyConversionBothWays(table, 75f, 50F)
-        verifyConversionBothWays(table, 7.5f, 5F)
-        verifyConversionBothWays(table, 0f, 0F)
-    }
-
-    @Test
-    fun pastEndsUsesLastScalingFactor() {
-        val table = createTable(8f to 16f, 10f to 20f, 30f to 60f)
-        verifyConversionBothWays(table, 200f, 100F)
-        verifyConversionBothWays(table, 62f, 31F)
-        verifyConversionBothWays(table, 2000f, 1000F)
-        verifyConversionBothWays(table, 4000f, 2000F)
-        verifyConversionBothWays(table, 20000f, 10000F)
-    }
-
-    @Test
-    fun negativeSpIsNegativeDp() {
-        val table = createTable(8f to 16f, 10f to 20f, 30f to 60f)
-        verifyConversionBothWays(table, -2f, -1F)
-        verifyConversionBothWays(table, -16f, -8F)
-        verifyConversionBothWays(table, -20f, -10F)
-        verifyConversionBothWays(table, -60f, -30F)
-        verifyConversionBothWays(table, -40f, -20F)
-        verifyConversionBothWays(table, -10f, -5F)
-        verifyConversionBothWays(table, 0f, -0F)
-    }
-
-    private fun createTable(vararg pairs: Pair<Float, Float>) =
-        FontScaleConverter(
-            pairs.map { it.first }.toFloatArray(),
-            pairs.map { it.second }.toFloatArray()
-        )
-
-    private fun verifyConversionBothWays(
-        table: FontScaleConverter,
-        expectedDp: Float,
-        spToConvert: Float
-    ) {
-        assertWithMessage("convertSpToDp")
-            .that(table.convertSpToDp(spToConvert))
-            .isWithin(CONVERSION_TOLERANCE)
-            .of(expectedDp)
-
-        assertWithMessage("inverse: convertDpToSp")
-            .that(table.convertDpToSp(expectedDp))
-            .isWithin(CONVERSION_TOLERANCE)
-            .of(spToConvert)
-    }
-
-    companion object {
-        private const val CONVERSION_TOLERANCE = 0.05f
-    }
-}
diff --git a/wear/protolayout/protolayout-material/build.gradle b/wear/protolayout/protolayout-material/build.gradle
index b1d7de5..be951af 100644
--- a/wear/protolayout/protolayout-material/build.gradle
+++ b/wear/protolayout/protolayout-material/build.gradle
@@ -49,7 +49,6 @@
     androidTestImplementation(libs.testRunner)
     androidTestImplementation("androidx.core:core:1.7.0")
     androidTestImplementation(project(":test:screenshot:screenshot"))
-    androidTestImplementation(libs.testUiautomator)
     androidTestImplementation(project(":wear:protolayout:protolayout-renderer"))
 
     testImplementation(libs.junit)
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
index c25e2f9..61734b9 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
@@ -16,13 +16,11 @@
 
 package androidx.wear.protolayout.material;
 
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_HEIGHT;
 import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_WIDTH;
 import static androidx.wear.protolayout.material.RunnerUtils.convertToTestParameters;
-import static androidx.wear.protolayout.material.RunnerUtils.getFontScale;
 import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
-import static androidx.wear.protolayout.material.RunnerUtils.setAndAssertFontScale;
-import static androidx.wear.protolayout.material.RunnerUtils.setFontScale;
 import static androidx.wear.protolayout.material.RunnerUtils.waitForNotificationToDisappears;
 import static androidx.wear.protolayout.material.TestCasesGenerator.XXXL_SCALE_SUFFIX;
 import static androidx.wear.protolayout.material.TestCasesGenerator.generateTestCases;
@@ -36,13 +34,10 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.screenshot.AndroidXScreenshotTestRule;
-import androidx.test.uiautomator.UiDevice;
 import androidx.wear.protolayout.DeviceParametersBuilders;
 import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
 import androidx.wear.protolayout.material.RunnerUtils.TestCase;
 
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -55,9 +50,14 @@
 @RunWith(Parameterized.class)
 @LargeTest
 public class MaterialGoldenXLTest {
-    private static final float FONT_SCALE_XXXL = 1.24f;
+    /* We set DisplayMetrics in the data() method for creating test cases. However, when running all
+    tests together, first all parametrization (data()) methods are called, and then individual
+    tests, causing that actual DisplayMetrics will be different. So we need to restore it before
+    each test. */
+    private static final DisplayMetrics DISPLAY_METRICS_FOR_TEST = new DisplayMetrics();
+    private static final DisplayMetrics OLD_DISPLAY_METRICS = new DisplayMetrics();
 
-    private static float originalFontScale;
+    private static final float FONT_SCALE_XXXL = 1.24f;
 
     private final TestCase mTestCase;
     private final String mExpected;
@@ -66,13 +66,6 @@
     public AndroidXScreenshotTestRule mScreenshotRule =
             new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
 
-    @BeforeClass
-    public static void setUpClass() throws Exception {
-        setAndAssertFontScale(
-                UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()),
-                FONT_SCALE_XXXL);
-    }
-
     public MaterialGoldenXLTest(String expected, TestCase testCase) {
         mTestCase = testCase;
         mExpected = expected;
@@ -83,18 +76,27 @@
         return (int) ((px - 0.5f) / scale);
     }
 
+    @SuppressWarnings("deprecation")
     @Parameterized.Parameters(name = "{0}")
-    public static Collection<Object[]> data() throws Exception {
-        // These "parameters" methods are called before any parameterized test (from any class)
-        // executes. We set and later reset the font here to have the correct context during test
-        // generation. We later set and reset the font for the actual test in BeforeClass/AfterClass
-        // methods.
-        UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
-        originalFontScale = getFontScale(device);
-        setAndAssertFontScale(device, FONT_SCALE_XXXL);
-
+    public static Collection<Object[]> data() {
         Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        DisplayMetrics currentDisplayMetrics = new DisplayMetrics();
         DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+        currentDisplayMetrics.setTo(displayMetrics);
+        displayMetrics.scaledDensity *= FONT_SCALE_XXXL;
+
+        InstrumentationRegistry.getInstrumentation()
+                .getContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+        InstrumentationRegistry.getInstrumentation()
+                .getTargetContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+
+        DISPLAY_METRICS_FOR_TEST.setTo(displayMetrics);
 
         float scale = displayMetrics.density;
         DeviceParameters deviceParameters =
@@ -102,7 +104,6 @@
                         .setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
                         .setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
                         .setScreenDensity(displayMetrics.density)
-                        .setFontScale(context.getResources().getConfiguration().fontScale)
                         // Not important for components.
                         .setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
                         .build();
@@ -125,17 +126,31 @@
                         /* isForLtr= */ true));
 
         // Restore state before this method, so other test have correct context.
-        setAndAssertFontScale(device, originalFontScale);
+        InstrumentationRegistry.getInstrumentation()
+                .getContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(currentDisplayMetrics);
+        InstrumentationRegistry.getInstrumentation()
+                .getTargetContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(currentDisplayMetrics);
+
         waitForNotificationToDisappears();
 
         return testCaseList;
     }
 
-    @AfterClass
-    public static void tearDownClass() throws Exception {
-        setFontScale(
-                UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()),
-                originalFontScale);
+    @Parameterized.BeforeParam
+    public static void restoreBefore() {
+        OLD_DISPLAY_METRICS.setTo(getApplicationContext().getResources().getDisplayMetrics());
+        getApplicationContext().getResources().getDisplayMetrics().setTo(DISPLAY_METRICS_FOR_TEST);
+    }
+
+    @Parameterized.AfterParam
+    public static void restoreAfter() {
+        getApplicationContext().getResources().getDisplayMetrics().setTo(OLD_DISPLAY_METRICS);
     }
 
     @Test
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/RunnerUtils.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/RunnerUtils.java
index 5ba7a7b..0fe42d3 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/RunnerUtils.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/RunnerUtils.java
@@ -16,8 +16,6 @@
 
 package androidx.wear.protolayout.material;
 
-import static org.junit.Assert.assertEquals;
-
 import android.annotation.SuppressLint;
 import android.app.Activity;
 import android.content.Intent;
@@ -30,13 +28,11 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.screenshot.AndroidXScreenshotTestRule;
 import androidx.test.screenshot.matchers.MSSIMMatcher;
-import androidx.test.uiautomator.UiDevice;
 import androidx.wear.protolayout.LayoutElementBuilders.Layout;
 import androidx.wear.protolayout.material.test.GoldenTestActivity;
 
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.stream.Collectors;
 
 public class RunnerUtils {
@@ -157,23 +153,4 @@
             this.isForLtr = isForLtr;
         }
     }
-
-    public static float getFontScale(UiDevice device) throws Exception {
-        String result = device.executeShellCommand("settings get system font_scale");
-        try {
-            return Float.parseFloat(result);
-        } catch (NumberFormatException e) {
-            return 1.0f;
-        }
-    }
-
-    public static void setFontScale(UiDevice device, float fontScale) throws Exception {
-        device.executeShellCommand("settings put system font_scale " + fontScale);
-    }
-
-    public static void setAndAssertFontScale(UiDevice device, float fontScale) throws Exception {
-        setFontScale(device, fontScale);
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
-        assertEquals(getFontScale(device), fontScale, 0.0001f);
-    }
 }
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
index af368f2..3c49e92 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
@@ -16,13 +16,11 @@
 
 package androidx.wear.protolayout.material.layouts;
 
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
 import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_HEIGHT;
 import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_WIDTH;
 import static androidx.wear.protolayout.material.RunnerUtils.convertToTestParameters;
-import static androidx.wear.protolayout.material.RunnerUtils.getFontScale;
 import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
-import static androidx.wear.protolayout.material.RunnerUtils.setAndAssertFontScale;
-import static androidx.wear.protolayout.material.RunnerUtils.setFontScale;
 import static androidx.wear.protolayout.material.RunnerUtils.waitForNotificationToDisappears;
 import static androidx.wear.protolayout.material.layouts.TestCasesGenerator.XXXL_SCALE_SUFFIX;
 import static androidx.wear.protolayout.material.layouts.TestCasesGenerator.generateTestCases;
@@ -34,13 +32,10 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.screenshot.AndroidXScreenshotTestRule;
-import androidx.test.uiautomator.UiDevice;
 import androidx.wear.protolayout.DeviceParametersBuilders;
 import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
 import androidx.wear.protolayout.material.RunnerUtils.TestCase;
 
-import org.junit.AfterClass;
-import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,9 +47,14 @@
 @RunWith(Parameterized.class)
 @LargeTest
 public class LayoutsGoldenXLTest {
-    private static final float FONT_SCALE_XXXL = 1.24f;
+    /* We set DisplayMetrics in the data() method for creating test cases. However, when running all
+    tests together, first all parametrization (data()) methods are called, and then individual
+    tests, causing that actual DisplayMetrics will be different. So we need to restore it before
+    each test. */
+    private static final DisplayMetrics DISPLAY_METRICS_FOR_TEST = new DisplayMetrics();
+    private static final DisplayMetrics OLD_DISPLAY_METRICS = new DisplayMetrics();
 
-    private static float originalFontScale;
+    private static final float FONT_SCALE_XXXL = 1.24f;
 
     private final TestCase mTestCase;
     private final String mExpected;
@@ -63,13 +63,6 @@
     public AndroidXScreenshotTestRule mScreenshotRule =
             new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
 
-    @BeforeClass
-    public static void setUpClass() throws Exception {
-        setAndAssertFontScale(
-                UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()),
-                FONT_SCALE_XXXL);
-    }
-
     public LayoutsGoldenXLTest(String expected, TestCase testCase) {
         mTestCase = testCase;
         mExpected = expected;
@@ -80,18 +73,27 @@
         return (int) ((px - 0.5f) / scale);
     }
 
+    @SuppressWarnings("deprecation")
     @Parameterized.Parameters(name = "{0}")
-    public static Collection<Object[]> data() throws Exception {
-        // These "parameters" methods are called before any parameterized test (from any class)
-        // executes. We set and later reset the font here to have the correct context during test
-        // generation. We later set and reset the font for the actual test in BeforeClass/AfterClass
-        // methods.
-        UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
-        originalFontScale = getFontScale(device);
-        setAndAssertFontScale(device, FONT_SCALE_XXXL);
-
+    public static Collection<Object[]> data() {
         Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        DisplayMetrics currentDisplayMetrics = new DisplayMetrics();
         DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+        currentDisplayMetrics.setTo(displayMetrics);
+        displayMetrics.scaledDensity *= FONT_SCALE_XXXL;
+
+        InstrumentationRegistry.getInstrumentation()
+                .getContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+        InstrumentationRegistry.getInstrumentation()
+                .getTargetContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+
+        DISPLAY_METRICS_FOR_TEST.setTo(displayMetrics);
 
         float scale = displayMetrics.density;
         DeviceParameters deviceParameters =
@@ -99,7 +101,6 @@
                         .setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
                         .setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
                         .setScreenDensity(displayMetrics.density)
-                        .setFontScale(context.getResources().getConfiguration().fontScale)
                         // TODO(b/231543947): Add test cases for round screen.
                         .setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
                         .build();
@@ -110,18 +111,40 @@
                         /* isForRtl= */ true,
                         /* isForLtr= */ true);
 
-        // Restore state before this method, so other test have correct context.
-        setAndAssertFontScale(device, originalFontScale);
+        // Restore state before this method, so other test have correct context. This is needed here
+        // too, besides in restoreBefore and restoreAfter as the test cases builder uses the context
+        // to apply font scaling, so we need that display metrics passed in. However, after
+        // generating cases we need to restore the state as other data() methods in this package can
+        // work correctly with the default state, as when the tests are run, first all data() static
+        // methods are called, and then parameterized test cases.
+        InstrumentationRegistry.getInstrumentation()
+                .getContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(currentDisplayMetrics);
+        InstrumentationRegistry.getInstrumentation()
+                .getTargetContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(currentDisplayMetrics);
         waitForNotificationToDisappears();
 
         return testCaseList;
     }
 
-    @AfterClass
-    public static void tearDownClass() throws Exception {
-        setFontScale(
-                UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()),
-                originalFontScale);
+    @Parameterized.BeforeParam
+    public static void restoreBefore() {
+        // Set the state as it was in data() method when we generated test cases. This was
+        // overridden by other static data() methods, so we need to restore it.
+        OLD_DISPLAY_METRICS.setTo(getApplicationContext().getResources().getDisplayMetrics());
+        getApplicationContext().getResources().getDisplayMetrics().setTo(DISPLAY_METRICS_FOR_TEST);
+    }
+
+    @Parameterized.AfterParam
+    public static void restoreAfter() {
+        // Restore the state to default, so the other tests and emulator have the correct starter
+        // state.
+        getApplicationContext().getResources().getDisplayMetrics().setTo(OLD_DISPLAY_METRICS);
     }
 
     @Test
diff --git a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/Typography.java b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/Typography.java
index da9219f..9e1613e 100644
--- a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/Typography.java
+++ b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/Typography.java
@@ -16,9 +16,6 @@
 
 package androidx.wear.protolayout.material;
 
-import static android.os.Build.VERSION.SDK_INT;
-import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
-
 import static androidx.annotation.Dimension.DP;
 import static androidx.annotation.Dimension.SP;
 import static androidx.wear.protolayout.DimensionBuilders.sp;
@@ -31,6 +28,7 @@
 
 import android.annotation.SuppressLint;
 import android.content.Context;
+import android.util.DisplayMetrics;
 
 import androidx.annotation.Dimension;
 import androidx.annotation.IntDef;
@@ -42,8 +40,6 @@
 import androidx.wear.protolayout.LayoutElementBuilders.FontStyle;
 import androidx.wear.protolayout.LayoutElementBuilders.FontVariant;
 import androidx.wear.protolayout.LayoutElementBuilders.FontWeight;
-import androidx.wear.protolayout.materialcore.fontscaling.FontScaleConverter;
-import androidx.wear.protolayout.materialcore.fontscaling.FontScaleConverterFactory;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -124,9 +120,6 @@
         TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_CAPTION2, 16f);
         TYPOGRAPHY_TO_LINE_HEIGHT_SP.put(TYPOGRAPHY_CAPTION3, 14f);
     }
-
-    private Typography() {}
-
     /**
      * Returns the {@link FontStyle.Builder} for the given FontStyle code with the recommended size,
      * weight and letter spacing. Font will be scalable.
@@ -137,6 +130,8 @@
         return getFontStyleBuilder(fontStyleCode, context, true);
     }
 
+    private Typography() {}
+
     /**
      * Returns the {@link FontStyle.Builder} for the given Typography code with the recommended
      * size, weight and letter spacing, with the option to make this font not scalable.
@@ -188,29 +183,17 @@
         return sp(checkNotNull(TYPOGRAPHY_TO_LINE_HEIGHT_SP.get(typography)).intValue());
     }
 
-    /**
-     * This is a helper function to make the font not scalable. It should interpret in value as DP
-     * and convert it to SP which is needed to be passed in as a font size. However, we will pass an
-     * SP object to it, because the default style is defined in it, but for the case when the font
-     * size on device is 1, so the DP is equal to SP.
-     */
-    @Dimension(unit = SP)
-    private static float dpToSp(float fontScale, @Dimension(unit = DP) float valueDp) {
-        FontScaleConverter converter =
-                (SDK_INT >= UPSIDE_DOWN_CAKE)
-                        ? FontScaleConverterFactory.forScale(fontScale)
-                        : null;
-
-        if (converter == null) {
-            return dpToSpLinear(fontScale, valueDp);
-        }
-
-        return converter.convertDpToSp(valueDp);
-    }
-
-    @Dimension(unit = SP)
-    private static float dpToSpLinear(float fontScale, @Dimension(unit = DP) float valueDp) {
-        return valueDp / fontScale;
+    @NonNull
+    @SuppressLint("ResourceType")
+    @SuppressWarnings("deprecation") // scaledDensity, b/335215227
+    // This is a helper function to make the font not scalable. It should interpret in value as DP
+    // and convert it to SP which is needed to be passed in as a font size. However, we will pass an
+    // SP object to it, because the default style is defined in it, but for the case when the font
+    // size on device in 1, so the DP is equal to SP.
+    private static SpProp dpToSp(@NonNull Context context, @Dimension(unit = DP) float valueDp) {
+        DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+        float scaledSp = (valueDp / metrics.scaledDensity) * metrics.density;
+        return sp(scaledSp);
     }
 
     // The @Dimension(unit = SP) on sp() is seemingly being ignored, so lint complains that we're
@@ -223,9 +206,8 @@
             float letterSpacing,
             boolean isScalable,
             @NonNull Context context) {
-        float fontScale = context.getResources().getConfiguration().fontScale;
         return new FontStyle.Builder()
-                .setSize(DimensionBuilders.sp(isScalable ? size : dpToSp(fontScale, size)))
+                .setSize(isScalable ? DimensionBuilders.sp(size) : dpToSp(context, size))
                 .setLetterSpacing(DimensionBuilders.em(letterSpacing))
                 .setVariant(variant)
                 .setWeight(weight);