Merge "Auto-grant permission used by ActivityCompatTest" into androidx-main
diff --git a/.github/ci-control/ci-config.json b/.github/ci-control/ci-config.json
index 4423f47..ea425a6 100644
--- a/.github/ci-control/ci-config.json
+++ b/.github/ci-control/ci-config.json
@@ -13,7 +13,6 @@
     },
     "main" : {
         "exclude" : [
-            "lifecycle"
         ],
         "default": true
     }
diff --git a/appcompat/appcompat/build.gradle b/appcompat/appcompat/build.gradle
index 81f3292..9c66ff5 100644
--- a/appcompat/appcompat/build.gradle
+++ b/appcompat/appcompat/build.gradle
@@ -18,7 +18,7 @@
 
     // Required to make activity 1.5.0-rc01 dependencies resolve.
     implementation("androidx.core:core-ktx:1.8.0")
-    implementation("org.jetbrains.kotlin:kotlin-stdlib:1.7.0")
+    implementation(libs.kotlinStdlib)
 
     implementation(projectOrArtifact(":emoji2:emoji2"))
     implementation(projectOrArtifact(":emoji2:emoji2-views-helper"))
@@ -36,7 +36,7 @@
     // Due to experimental annotations used in core.
     compileOnly(libs.kotlinStdlib)
 
-    kapt(projectOrArtifact(":resourceinspection:resourceinspection-processor"))
+    kapt("androidx.resourceinspection:resourceinspection-annotation:1.0.1")
 
     androidTestImplementation(libs.kotlinStdlib)
     androidTestImplementation(libs.testExtJunit)
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/view/ContextThemeWrapper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/view/ContextThemeWrapper.java
index b8c582d..07064be 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/view/ContextThemeWrapper.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/view/ContextThemeWrapper.java
@@ -118,7 +118,9 @@
 
     private Resources getResourcesInternal() {
         if (mResources == null) {
-            if (isEmptyConfiguration(mOverrideConfiguration)) {
+            if (mOverrideConfiguration == null
+                    || (Build.VERSION.SDK_INT >= 26
+                    && isEmptyConfiguration(mOverrideConfiguration))) {
                 // If we're not applying any overrides, use the base context's resources. On API
                 // 26+, this will avoid pulling in resources that share a backing implementation
                 // with the application context.
@@ -215,6 +217,7 @@
      * @return {@code true} if the specified configuration is {@code null} or is a no-op when
      *         used as a configuration overlay
      */
+    @RequiresApi(26)
     private static boolean isEmptyConfiguration(Configuration overrideConfiguration) {
         if (overrideConfiguration == null) {
             return true;
diff --git a/bluetooth/bluetooth/build.gradle b/bluetooth/bluetooth/build.gradle
index 9bf971b..a83aefa 100644
--- a/bluetooth/bluetooth/build.gradle
+++ b/bluetooth/bluetooth/build.gradle
@@ -24,6 +24,7 @@
 
 dependencies {
     implementation(libs.kotlinStdlib)
+    implementation 'androidx.annotation:annotation:1.4.0'
 }
 
 androidx {
@@ -36,4 +37,7 @@
 
 android {
     namespace "androidx.bluetooth"
+    defaultConfig {
+        minSdkVersion 21
+    }
 }
diff --git a/bluetooth/bluetooth/src/main/AndroidManifest.xml b/bluetooth/bluetooth/src/main/AndroidManifest.xml
index 6abff90..24c3903 100644
--- a/bluetooth/bluetooth/src/main/AndroidManifest.xml
+++ b/bluetooth/bluetooth/src/main/AndroidManifest.xml
@@ -13,4 +13,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-
\ No newline at end of file
+
+
+    
+
\ No newline at end of file
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothGattDescriptor.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothGattDescriptor.kt
new file mode 100644
index 0000000..2aaf3a5
--- /dev/null
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothGattDescriptor.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.bluetooth
+
+import android.os.Build
+import android.os.Bundle
+import androidx.annotation.RequiresApi
+import androidx.bluetooth.utils.Bundleable
+
+import java.util.UUID
+
+/**
+ * @hide
+ */
+class BluetoothGattDescriptor internal constructor(
+    descriptor: android.bluetooth.BluetoothGattDescriptor
+) : Bundleable {
+    private val impl: GattDescriptorImpl =
+        if (Build.VERSION.SDK_INT >= 24) {
+            GattDescriptorImplApi24(descriptor)
+        } else {
+            GattDescriptorImplApi21(descriptor)
+        }
+    internal val fwkDescriptor: android.bluetooth.BluetoothGattDescriptor
+        get() = impl.fwkDescriptor
+    val permissions: Int
+        get() = impl.permissions
+    val uuid: UUID
+        get() = impl.uuid
+    val characteristic: android.bluetooth.BluetoothGattCharacteristic?
+        get() = impl.characteristic
+
+    constructor(uuid: UUID, permissions: Int) : this(
+        android.bluetooth.BluetoothGattDescriptor(
+            uuid,
+            permissions
+        )
+    )
+
+    companion object {
+        const val PERMISSION_READ = android.bluetooth.BluetoothGattDescriptor.PERMISSION_READ
+        const val PERMISSION_READ_ENCRYPTED =
+            android.bluetooth.BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED
+        const val PERMISSION_READ_ENCRYPTED_MITM =
+            android.bluetooth.BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED_MITM
+        const val PERMISSION_WRITE = android.bluetooth.BluetoothGattDescriptor.PERMISSION_WRITE
+        const val PERMISSION_WRITE_ENCRYPTED =
+            android.bluetooth.BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED
+        const val PERMISSION_WRITE_ENCRYPTED_MITM =
+            android.bluetooth.BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED_MITM
+        const val PERMISSION_WRITE_SIGNED =
+            android.bluetooth.BluetoothGattDescriptor.PERMISSION_WRITE_SIGNED
+        const val PERMISSION_WRITE_SIGNED_MITM =
+            android.bluetooth.BluetoothGattDescriptor.PERMISSION_WRITE_SIGNED_MITM
+
+        val ENABLE_NOTIFICATION_VALUE = byteArrayOf(0x01, 0x00)
+        val ENABLE_INDICATION_VALUE = byteArrayOf(0x02, 0x00)
+        val DISABLE_NOTIFICATION_VALUE = byteArrayOf(0x00, 0x00)
+
+        internal fun keyForField(field: Int): String? {
+            return field.toString(Character.MAX_RADIX)
+        }
+
+        val CREATOR: Bundleable.Creator =
+            if (Build.VERSION.SDK_INT >= 24) GattDescriptorImplApi24.CREATOR
+            else GattDescriptorImplApi21.CREATOR
+    }
+
+    override fun toBundle(): Bundle {
+        return impl.toBundle()
+    }
+
+    private interface GattDescriptorImpl {
+        val fwkDescriptor: android.bluetooth.BluetoothGattDescriptor
+        val permissions: Int
+        val uuid: UUID
+        val characteristic: android.bluetooth.BluetoothGattCharacteristic?
+        fun toBundle(): Bundle
+    }
+
+    private open class GattDescriptorImplApi21(
+        descriptor: android.bluetooth.BluetoothGattDescriptor
+    ) : GattDescriptorImpl {
+        companion object {
+
+            internal const val FIELD_FWK_DESCRIPTOR_PERMISSIONS = 1
+            internal const val FIELD_FWK_DESCRIPTOR_UUID = 2
+            internal const val FIELD_FWK_DESCRIPTOR_INSTANCE = 3
+
+            val CREATOR: Bundleable.Creator =
+                object : Bundleable.Creator {
+
+                    @Suppress("DEPRECATION")
+                    override fun fromBundle(bundle: Bundle): BluetoothGattDescriptor {
+                        val permissions =
+                            bundle.getInt(
+                                keyForField(FIELD_FWK_DESCRIPTOR_PERMISSIONS),
+                                -1
+                            )
+                        val uuid = bundle.getString(
+                            keyForField(FIELD_FWK_DESCRIPTOR_UUID),
+                        ) ?: throw IllegalArgumentException("Bundle doesn't include uuid")
+
+                        if (permissions == -1) {
+                            throw IllegalArgumentException("Bundle doesn't include permission")
+                        }
+
+                        val descriptor =
+                            android.bluetooth.BluetoothGattDescriptor(
+                                UUID.fromString(uuid),
+                                permissions
+                            )
+
+                        descriptor.javaClass.getDeclaredField("mInstance").setInt(
+                            descriptor, bundle.getInt(
+                                keyForField(FIELD_FWK_DESCRIPTOR_INSTANCE), 0
+                            )
+                        )
+                        return BluetoothGattDescriptor(descriptor)
+                    }
+                }
+        }
+
+        override val fwkDescriptor: android.bluetooth.BluetoothGattDescriptor = descriptor
+        override val permissions: Int
+            get() = fwkDescriptor.permissions
+        override val uuid: UUID
+            get() = fwkDescriptor.uuid
+        override val characteristic: android.bluetooth.BluetoothGattCharacteristic?
+            get() = fwkDescriptor.characteristic
+
+        override fun toBundle(): Bundle {
+            val bundle = Bundle()
+            bundle.putString(keyForField(FIELD_FWK_DESCRIPTOR_UUID), uuid.toString())
+            bundle.putInt(keyForField(FIELD_FWK_DESCRIPTOR_PERMISSIONS), permissions)
+            val instanceId: Int =
+                fwkDescriptor.javaClass.getDeclaredField("mInstance").getInt(fwkDescriptor)
+            bundle.putInt(keyForField(FIELD_FWK_DESCRIPTOR_INSTANCE), instanceId)
+            return bundle
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.N)
+    private open class GattDescriptorImplApi24(
+        descriptor: android.bluetooth.BluetoothGattDescriptor
+    ) : GattDescriptorImplApi21(descriptor) {
+        companion object {
+            internal const val FIELD_FWK_DESCRIPTOR = 0
+            val CREATOR: Bundleable.Creator =
+                object : Bundleable.Creator {
+                    @Suppress("DEPRECATION")
+                    override fun fromBundle(bundle: Bundle): BluetoothGattDescriptor {
+                        val fwkDescriptor =
+                            bundle.getParcelable(
+                                keyForField(FIELD_FWK_DESCRIPTOR)
+                            ) ?: throw IllegalArgumentException("Bundle doesn't contain descriptor")
+                        return BluetoothGattDescriptor(fwkDescriptor)
+                    }
+                }
+        }
+
+        override fun toBundle(): Bundle {
+            val bundle = Bundle()
+            bundle.putParcelable(
+                keyForField(FIELD_FWK_DESCRIPTOR),
+                fwkDescriptor
+            )
+            return bundle
+        }
+    }
+}
\ No newline at end of file
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/Bundleable.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/Bundleable.kt
new file mode 100644
index 0000000..73440dd
--- /dev/null
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/utils/Bundleable.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.bluetooth.utils
+
+import android.os.Bundle
+
+/**
+ * @hide
+ */
+interface Bundleable {
+    /** Returns a [Bundle] representing the information stored in this object.  */
+    fun toBundle(): Bundle
+
+    /** Interface for the static `CREATOR` field of [Bundleable] classes.  */
+    interface Creator {
+        /**
+         * Restores a [Bundleable] instance from a [Bundle] produced by [ ][Bundleable.toBundle].
+         *
+         *
+         * It guarantees the compatibility of [Bundle] representations produced by different
+         * versions of [Bundleable.toBundle] by providing best default values for missing
+         * fields. It throws an exception if any essential fields are missing.
+         */
+        fun fromBundle(bundle: Bundle): T
+    }
+}
\ No newline at end of file
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXPluginTestContext.kt b/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXPluginTestContext.kt
index 94dd541..35c08d3 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXPluginTestContext.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/AndroidXPluginTestContext.kt
@@ -165,16 +165,21 @@
     fun AndroidXSelfTestProject.readPublishedFile(fileName: String) =
         mavenLocalDir.resolve("$groupId/$artifactId/$version/$fileName").readText()
 
+    var printBuildFileOnFailure: Boolean = false
+
     override fun toString(): String {
         return buildMap {
             put("root files", setup.rootDir.list().orEmpty().toList())
-            setup.rootDir.listFiles().orEmpty().filter { it.isDirectory }.forEach { maybeGroupDir ->
-                maybeGroupDir.listFiles().orEmpty().filter { it.isDirectory }.forEach {
-                    val maybeBuildFile = it.resolve("build.gradle")
-                    if (maybeBuildFile.exists()) {
-                        put(it.name + "/build.gradle", maybeBuildFile.readText())
+            if (printBuildFileOnFailure) {
+                setup.rootDir.listFiles().orEmpty().filter { it.isDirectory }
+                    .forEach { maybeGroupDir ->
+                        maybeGroupDir.listFiles().orEmpty().filter { it.isDirectory }.forEach {
+                            val maybeBuildFile = it.resolve("build.gradle")
+                            if (maybeBuildFile.exists()) {
+                                put(it.name + "/build.gradle", maybeBuildFile.readText())
+                            }
+                        }
                     }
-                }
             }
         }.toString()
     }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 2a58f28d..89a23c4 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -51,7 +51,6 @@
 import com.android.build.gradle.TestExtension
 import com.android.build.gradle.TestPlugin
 import com.android.build.gradle.TestedExtension
-import com.android.build.gradle.internal.lint.AndroidLintTask
 import com.android.build.gradle.internal.tasks.AnalyticsRecordingTask
 import com.android.build.gradle.internal.tasks.ListingFileRedirectTask
 import org.gradle.api.GradleException
@@ -383,17 +382,6 @@
             }
         }
 
-        // Remove the lint and column attributes from generated lint baseline XML.
-        project.tasks.withType(AndroidLintTask::class.java).configureEach { task ->
-            if (task.name.startsWith("updateLintBaseline")) {
-                task.doLast {
-                    task.outputs.files.find { it.name == "lint-baseline.xml" }?.let { file ->
-                        file.writeText(removeLineAndColumnAttributes(file.readText()))
-                    }
-                }
-            }
-        }
-
         // Remove the android:targetSdkVersion element from the manifest used for AARs.
         project.extensions.getByType().onVariants { variant ->
             project.tasks.register(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
index bc5938e..fb481768 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXMultiplatformExtension.kt
@@ -95,17 +95,19 @@
         } else { null }
     }
 
+    /**
+     * Configures all ios targets supported by AndroidX.
+     */
     @JvmOverloads
-    fun iosArm32(
+    fun ios(
         block: Action? = null
-    ): KotlinNativeTarget? {
-        return if (project.enableMac()) {
-            kotlinExtension.iosArm32().also {
-                block?.execute(it)
-            }
-        } else { null }
+    ): List {
+        return listOfNotNull(
+            iosX64(block),
+            iosArm64(block),
+            iosSimulatorArm64(block)
+        )
     }
-
     @JvmOverloads
     fun iosX64(
         block: Action? = null
@@ -118,6 +120,17 @@
     }
 
     @JvmOverloads
+    fun iosSimulatorArm64(
+        block: Action? = null
+    ): KotlinNativeTarget? {
+        return if (project.enableMac()) {
+            kotlinExtension.iosSimulatorArm64().also {
+                block?.execute(it)
+            }
+        } else { null }
+    }
+
+    @JvmOverloads
     fun linuxX64(
         block: Action? = null
     ): KotlinNativeTargetWithHostTests? {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index fb9439f..fe5eb3d 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -41,7 +41,6 @@
 import org.gradle.api.tasks.bundling.ZipEntryCompression
 import org.gradle.build.event.BuildEventsListenerRegistry
 import org.gradle.kotlin.dsl.extra
-import org.jetbrains.kotlin.gradle.plugin.sources.CleanupStaleSourceSetMetadataEntriesService
 
 abstract class AndroidXRootImplPlugin : Plugin {
     @Suppress("UnstableApiUsage")
@@ -58,7 +57,6 @@
     private fun Project.configureRootProject() {
         project.validateAllAndroidxArgumentsAreRecognized()
         tasks.register("listAndroidXProperties", ListAndroidXPropertiesTask::class.java)
-        this.disableKmpKlibCleanupService()
         setDependencyVersions()
         configureKtlintCheckFile()
         tasks.register(CheckExternalDependencyLicensesTask.TASK_NAME)
@@ -240,35 +238,4 @@
         androidx.build.dependencies.agpVersion = getVersionByName("androidGradlePlugin")
         androidx.build.dependencies.guavaVersion = getVersionByName("guavaJre")
     }
-
-    /**
-     * This function applies the workaround for b/236850628.
-     * There is a BuildService in KMP Gradle Plugin 1.7.0 that tries to cleanup unused klibs,
-     * which is over-eager and causes missing dependencies in the Studio UI. This function simply
-     * breaks the inputs of that cleanup function by intercepting the service creation and changing
-     * its parameters.
-     *
-     * That code is already removed in KMP main branch and we should be able to remove this
-     * workaround after updating to 1.7.20.
-     *
-     * see b/236850628#comment25 for more details.
-     */
-    private fun Project.disableKmpKlibCleanupService() {
-        project.gradle.sharedServices.registrations.whenObjectAdded { serviceRegistration ->
-            if (serviceRegistration.name == "cleanup-stale-sourceset-metadata") {
-                val params = serviceRegistration.parameters as
-                    CleanupStaleSourceSetMetadataEntriesService.Parameters
-                val tmpDirectory = project.layout.buildDirectory.dir("klib-cleanup-workaround")
-                params.projectStorageRoot.set(
-                    tmpDirectory.map { it.asFile }
-                )
-                params.projectStorageDirectories.set(emptyMap())
-                // make sure the store root is not changed afterwards. we don't need to set this
-                // for projectStorageDirectories as the files are deleted from projectStorageRoot,
-                // not projectStorageDirectories.
-                // https://github.com/JetBrains/kotlin/blob/47beef16f38ea315bf4d7d4d3faf34b0b952b613/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/SourceSetMetadataStorageForIde.kt#L26
-                params.projectStorageRoot.disallowChanges()
-            }
-        }
-    }
 }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index 2c76d84..288c32b8 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -19,6 +19,7 @@
 import androidx.build.dependencyTracker.AffectedModuleDetector
 import com.android.build.api.dsl.Lint
 import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask
+import com.android.build.gradle.internal.lint.AndroidLintTask
 import java.io.File
 import java.util.Locale
 import org.gradle.api.GradleException
@@ -171,6 +172,17 @@
         )
     }
 
+    tasks.withType(AndroidLintTask::class.java).configureEach { task ->
+        // Remove the lint and column attributes from generated lint baseline XML.
+        if (task.name.startsWith("updateLintBaseline")) {
+            task.doLast {
+                task.outputs.files.find { it.name == "lint-baseline.xml" }?.let { file ->
+                    file.writeText(removeLineAndColumnAttributes(file.readText()))
+                }
+            }
+        }
+    }
+
     // Lint is configured entirely in finalizeDsl so that individual projects cannot easily
     // disable individual checks in the DSL for any reason.
     lint.apply {
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/PublishExtension.kt b/buildSrc/public/src/main/kotlin/androidx/build/PublishExtension.kt
index 5f2c434..0b79b6d 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/PublishExtension.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/PublishExtension.kt
@@ -25,6 +25,7 @@
     var js = Publish.UNSET
     var mac = Publish.UNSET
     var linux = Publish.UNSET
+    var ios = Publish.UNSET
 
     /**
      * List of platforms names which should be published to maven. e.g. ["jvm", "js"]
@@ -44,10 +45,13 @@
             if (linux.shouldPublish()) {
                 platforms.addAll(linuxPlatforms)
             }
+            if (ios.shouldPublish()) {
+                platforms.addAll(iosPlatforms)
+            }
             return platforms
         }
     private val allExtendedPlatforms
-        get() = listOf(jvm, js, mac, linux)
+        get() = listOf(jvm, js, mac, linux, ios)
     private val allPlatforms
         get() = listOf(android) + allExtendedPlatforms
     private val activeExtendedPlatforms
@@ -73,7 +77,11 @@
         private const val MAC_ARM_64 = "macosarm64"
         private const val MAC_OSX_64 = "macosx64"
         private const val LINUX_64 = "linuxx64"
+        private const val IOS_SIMULATOR_ARM_64 = "iossimulatorarm64"
+        private const val IOS_X_64 = "iosx64"
+        private const val IOS_ARM_64 = "iosarm64"
         private val macPlatforms = listOf(MAC_ARM_64, MAC_OSX_64)
         private val linuxPlatforms = listOf(LINUX_64)
+        private val iosPlatforms = listOf(IOS_SIMULATOR_ARM_64, IOS_ARM_64, IOS_X_64)
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
index a524d23..5f74052 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
@@ -45,11 +45,16 @@
  * [android.hardware.camera2.CameraDevice] and [android.hardware.camera2.CameraCaptureSession] via
  * the [CameraGraph] interface.
  */
-public class CameraPipe(config: Config, threadConfig: ThreadConfig = ThreadConfig()) {
+public class CameraPipe(config: Config) {
+
+    @Deprecated("threadConfig should be specified on config.")
+    @Suppress("UNUSED_PARAMETER")
+    public constructor(config: Config, threadConfig: ThreadConfig) : this(config)
+
     private val debugId = cameraPipeIds.incrementAndGet()
     private val component: CameraPipeComponent = DaggerCameraPipeComponent.builder()
         .cameraPipeConfigModule(CameraPipeConfigModule(config))
-        .threadConfigModule(ThreadConfigModule(threadConfig))
+        .threadConfigModule(ThreadConfigModule(config.threadConfig))
         .build()
 
     /**
diff --git a/car/app/app-automotive/api/current.txt b/car/app/app-automotive/api/current.txt
index fdfc2bc..e0b9e41 100644
--- a/car/app/app-automotive/api/current.txt
+++ b/car/app/app-automotive/api/current.txt
@@ -3,7 +3,7 @@
 
   public abstract class BaseCarAppActivity extends androidx.fragment.app.FragmentActivity implements androidx.lifecycle.LifecycleOwner {
     ctor public BaseCarAppActivity();
-    method public void bindToViewModel();
+    method public void bindToViewModel(androidx.car.app.SessionInfo);
     method public android.content.ComponentName? retrieveServiceComponentName();
   }
 
diff --git a/car/app/app-automotive/api/public_plus_experimental_current.txt b/car/app/app-automotive/api/public_plus_experimental_current.txt
index 83f0862..d7ebfbc 100644
--- a/car/app/app-automotive/api/public_plus_experimental_current.txt
+++ b/car/app/app-automotive/api/public_plus_experimental_current.txt
@@ -3,7 +3,7 @@
 
   public abstract class BaseCarAppActivity extends androidx.fragment.app.FragmentActivity implements androidx.lifecycle.LifecycleOwner {
     ctor public BaseCarAppActivity();
-    method public void bindToViewModel();
+    method public void bindToViewModel(androidx.car.app.SessionInfo);
     method public android.content.ComponentName? retrieveServiceComponentName();
   }
 
diff --git a/car/app/app-automotive/api/restricted_current.txt b/car/app/app-automotive/api/restricted_current.txt
index cf8af8b..9c3b7b0 100644
--- a/car/app/app-automotive/api/restricted_current.txt
+++ b/car/app/app-automotive/api/restricted_current.txt
@@ -3,7 +3,7 @@
 
   public abstract class BaseCarAppActivity extends androidx.fragment.app.FragmentActivity {
     ctor public BaseCarAppActivity();
-    method public void bindToViewModel();
+    method public void bindToViewModel(androidx.car.app.SessionInfo);
     method public android.content.ComponentName? retrieveServiceComponentName();
   }
 
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/BaseCarAppActivity.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/BaseCarAppActivity.java
index 9c0574f..13b2553 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/BaseCarAppActivity.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/BaseCarAppActivity.java
@@ -40,6 +40,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
+import androidx.car.app.SessionInfo;
 import androidx.car.app.activity.renderer.ICarAppActivity;
 import androidx.car.app.activity.renderer.IInsetsListener;
 import androidx.car.app.activity.renderer.IRendererCallback;
@@ -252,7 +253,7 @@
     /**
      * Binds the {@link BaseCarAppActivity} and it's view against the view model.
      */
-    public void bindToViewModel() {
+    public void bindToViewModel(@NonNull SessionInfo sessionInfo) {
         ComponentName serviceComponentName = retrieveServiceComponentName();
         if (serviceComponentName == null) {
             Log.e(LogTags.TAG, "Unspecified service class name");
@@ -261,7 +262,7 @@
         }
 
         CarAppViewModelFactory factory = CarAppViewModelFactory.getInstance(getApplication(),
-                serviceComponentName);
+                serviceComponentName, sessionInfo);
         mViewModel = new ViewModelProvider(this, factory).get(CarAppViewModel.class);
         mViewModel.setActivity(this);
         mViewModel.resetState();
@@ -329,9 +330,7 @@
     }
 
     private void onErrorChanged(@Nullable ErrorHandler.ErrorType errorType) {
-        ThreadUtils.runOnMain(() -> {
-            mErrorMessageView.setError(errorType);
-        });
+        ThreadUtils.runOnMain(() -> mErrorMessageView.setError(errorType));
     }
 
     private void onStateChanged(@NonNull CarAppViewModel.State state) {
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java
index faf5f42..c5d96a5 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppActivity.java
@@ -16,6 +16,10 @@
 
 package androidx.car.app.activity;
 
+import static androidx.car.app.SessionInfo.DISPLAY_TYPE_MAIN;
+
+import static java.lang.System.identityHashCode;
+
 import android.annotation.SuppressLint;
 import android.content.Intent;
 import android.os.Bundle;
@@ -23,6 +27,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.car.app.CarAppService;
+import androidx.car.app.SessionInfo;
 
 /**
  * The class representing a car app activity in the main display.
@@ -76,6 +81,15 @@
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        bindToViewModel();
+
+        String identifier;
+        if (getIntent().getIdentifier() != null) {
+            identifier = getIntent().getIdentifier();
+        } else {
+            identifier = String.valueOf(identityHashCode(this));
+        }
+
+        bindToViewModel(new SessionInfo(DISPLAY_TYPE_MAIN,
+                identifier));
     }
 }
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModel.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModel.java
index 9b64f77..a1737df 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModel.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModel.java
@@ -30,6 +30,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
+import androidx.car.app.SessionInfo;
 import androidx.car.app.activity.renderer.ICarAppActivity;
 import androidx.car.app.activity.renderer.IInsetsListener;
 import androidx.car.app.activity.renderer.IRendererCallback;
@@ -56,9 +57,12 @@
     private final MutableLiveData mError = new MutableLiveData<>();
     private final MutableLiveData mState = new MutableLiveData<>(State.IDLE);
     private ServiceConnectionManager mServiceConnectionManager;
-    @Nullable private IRendererCallback mIRendererCallback;
-    @Nullable private IInsetsListener mIInsetsListener;
-    @Nullable private Insets mInsets = Insets.NONE;
+    @Nullable
+    private IRendererCallback mIRendererCallback;
+    @Nullable
+    private IInsetsListener mIInsetsListener;
+    @Nullable
+    private Insets mInsets = Insets.NONE;
     private static WeakReference sActivity = new WeakReference<>(null);
 
     /** Possible view states */
@@ -73,14 +77,17 @@
         ERROR,
     }
 
-    public CarAppViewModel(@NonNull Application application, @NonNull ComponentName componentName) {
+    public CarAppViewModel(@NonNull Application application,
+            @NonNull ComponentName componentName, @NonNull SessionInfo sessionInfo) {
         super(application);
 
-        mServiceConnectionManager = new ServiceConnectionManager(application, componentName, this);
+        mServiceConnectionManager = new ServiceConnectionManager(application, componentName,
+                sessionInfo, this);
     }
 
     @VisibleForTesting
-    @NonNull ServiceConnectionManager getServiceConnectionManager() {
+    @NonNull
+    ServiceConnectionManager getServiceConnectionManager() {
         return mServiceConnectionManager;
     }
 
@@ -89,7 +96,8 @@
         mServiceConnectionManager = serviceConnectionManager;
     }
 
-    @NonNull ServiceDispatcher getServiceDispatcher() {
+    @NonNull
+    ServiceDispatcher getServiceDispatcher() {
         return mServiceConnectionManager.getServiceDispatcher();
     }
 
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModelFactory.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModelFactory.java
index 3c9627d..4ce892c 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModelFactory.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/CarAppViewModelFactory.java
@@ -23,6 +23,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.car.app.SessionInfo;
 import androidx.lifecycle.ViewModel;
 import androidx.lifecycle.ViewModelProvider;
 
@@ -40,11 +41,13 @@
 
     Application mApplication;
     ComponentName mComponentName;
+    SessionInfo mSessionInfo;
 
     private CarAppViewModelFactory(@NonNull ComponentName componentName,
-            @NonNull Application application) {
+            @NonNull Application application, @NonNull SessionInfo sessionInfo) {
         mComponentName = componentName;
         mApplication = application;
+        mSessionInfo = sessionInfo;
     }
 
     /**
@@ -54,10 +57,10 @@
      */
     @NonNull
     static CarAppViewModelFactory getInstance(Application application,
-            ComponentName componentName) {
+            ComponentName componentName, SessionInfo sessionInfo) {
         CarAppViewModelFactory instance = sInstances.get(componentName);
         if (instance == null) {
-            instance = new CarAppViewModelFactory(componentName, application);
+            instance = new CarAppViewModelFactory(componentName, application, sessionInfo);
             sInstances.put(componentName, instance);
         }
         return instance;
@@ -67,6 +70,6 @@
     @NonNull
     @Override
     public  T create(@NonNull Class modelClass) {
-        return (T) new CarAppViewModel(mApplication, mComponentName);
+        return (T) new CarAppViewModel(mApplication, mComponentName, mSessionInfo);
     }
 }
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceConnectionManager.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceConnectionManager.java
index 8d91a334..5d8ff94 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceConnectionManager.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceConnectionManager.java
@@ -36,6 +36,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.car.app.HandshakeInfo;
+import androidx.car.app.SessionInfo;
 import androidx.car.app.activity.renderer.ICarAppActivity;
 import androidx.car.app.activity.renderer.IRendererService;
 import androidx.car.app.versioning.CarAppApiLevels;
@@ -58,6 +59,7 @@
 
     final ServiceConnectionListener mListener;
     private final ComponentName mServiceComponentName;
+    private final SessionInfo mSessionInfo;
     private final Context mContext;
     private final ServiceDispatcher mServiceDispatcher;
     private int mDisplayId;
@@ -79,8 +81,10 @@
 
     public ServiceConnectionManager(@NonNull Context context,
             @NonNull ComponentName serviceComponentName,
+            @NonNull SessionInfo sessionInfo,
             @NonNull ServiceConnectionListener listener) {
         mContext = context;
+        mSessionInfo = sessionInfo;
         mListener = listener;
         mServiceComponentName = serviceComponentName;
         mServiceDispatcher = new ServiceDispatcher(listener, this::isBound);
@@ -197,6 +201,7 @@
         }
 
         Intent rendererIntent = new Intent(ACTION_RENDER);
+        SessionInfo.setBindData(rendererIntent, mSessionInfo);
         List resolveInfoList =
                 mContext.getPackageManager()
                         .queryIntentServices(rendererIntent, PackageManager.GET_META_DATA);
@@ -284,7 +289,7 @@
     private boolean updateIntent() {
         ComponentName serviceComponentName = requireNonNull(mServiceComponentName);
         Intent intent = requireNonNull(mIntent);
-
+        SessionInfo.setBindData(intent, mSessionInfo);
         IRendererService service = mRendererService;
         if (service == null) {
             Log.e(LogTags.TAG, "Service dispatcher is not connected");
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java
index dd0c09e..b7a7fc9 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java
@@ -19,6 +19,8 @@
 import static android.view.KeyEvent.ACTION_UP;
 import static android.view.KeyEvent.KEYCODE_R;
 
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.fail;
@@ -51,6 +53,7 @@
 import android.view.inputmethod.InputConnection;
 
 import androidx.car.app.CarAppService;
+import androidx.car.app.SessionInfo;
 import androidx.car.app.activity.renderer.ICarAppActivity;
 import androidx.car.app.activity.renderer.IInsetsListener;
 import androidx.car.app.activity.renderer.IProxyInputConnection;
@@ -64,7 +67,6 @@
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.ViewModelProvider;
 import androidx.test.core.app.ActivityScenario;
-import androidx.test.core.app.ApplicationProvider;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -79,19 +81,20 @@
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
 public class CarAppActivityTest {
-    private final ComponentName mRendererComponent = new ComponentName(
-            ApplicationProvider.getApplicationContext(), getClass().getName());
+    public static final String INTENT_IDENTIFIER = "CarAppActivityTest";
+    private final ComponentName mRendererComponent = new ComponentName(getApplicationContext(),
+            getClass().getName());
     private final String mFakeCarAppServiceClass = "com.fake.FakeCarAppService";
     private final ComponentName mFakeCarAppServiceComponent = new ComponentName(
-            ApplicationProvider.getApplicationContext(), mFakeCarAppServiceClass);
+            getApplicationContext(), mFakeCarAppServiceClass);
     private final IRendererService mRenderService = mock(IRendererService.class);
-    private final RenderServiceDelegate mRenderServiceDelegate =
-            new RenderServiceDelegate(mRenderService);
+    private final RenderServiceDelegate mRenderServiceDelegate = new RenderServiceDelegate(
+            mRenderService);
 
     @Before
     public void setup() {
         try {
-            Application app = ApplicationProvider.getApplicationContext();
+            Application app = getApplicationContext();
 
             // Register fake {@code CarAppService}
             PackageManager packageManager = app.getPackageManager();
@@ -105,8 +108,7 @@
             spm.addIntentFilterForService(mRendererComponent,
                     new IntentFilter(CarAppActivity.ACTION_RENDER));
 
-            when(mRenderService.initialize(any(ICarAppActivity.class),
-                    any(ComponentName.class),
+            when(mRenderService.initialize(any(ICarAppActivity.class), any(ComponentName.class),
                     anyInt())).thenReturn(true);
             when(mRenderService.onNewIntent(any(Intent.class), any(ComponentName.class),
                     anyInt())).thenReturn(true);
@@ -122,8 +124,7 @@
     @Test
     public void testRendererInitialization() {
         runOnActivity((scenario, activity) -> {
-            verify(mRenderService, times(1)).initialize(
-                    mRenderServiceDelegate.getCarAppActivity(),
+            verify(mRenderService, times(1)).initialize(mRenderServiceDelegate.getCarAppActivity(),
                     mFakeCarAppServiceComponent, activity.getDisplayId());
             verify(mRenderService, times(1)).onNewIntent(activity.getIntent(),
                     mFakeCarAppServiceComponent, activity.getDisplayId());
@@ -162,12 +163,10 @@
             verify(callback, times(1)).onResume();
 
             // Add a test-specific lifecycle callback to activity.
-            ActivityLifecycleCallbacks activityCallback = mock(
-                    ActivityLifecycleCallbacks.class);
+            ActivityLifecycleCallbacks activityCallback = mock(ActivityLifecycleCallbacks.class);
             activity.registerActivityLifecycleCallbacks(activityCallback);
             // Report service connection error.
-            CarAppViewModel viewModel =
-                    new ViewModelProvider(activity).get(CarAppViewModel.class);
+            CarAppViewModel viewModel = new ViewModelProvider(activity).get(CarAppViewModel.class);
             viewModel.onError(ErrorHandler.ErrorType.HOST_ERROR);
 
             assertThat(activity.isFinishing()).isEqualTo(false);
@@ -197,19 +196,17 @@
         runOnActivity((scenario, activity) -> {
             ServiceConnectionManager serviceConnectionManager =
                     activity.mViewModel.getServiceConnectionManager();
-            ServiceConnection serviceConnection =
-                    spy(serviceConnectionManager.getServiceConnection());
+            ServiceConnection serviceConnection = spy(
+                    serviceConnectionManager.getServiceConnection());
             serviceConnectionManager.setServiceConnection(serviceConnection);
 
             // Destroy activity to force unbind.
             scenario.moveToState(Lifecycle.State.DESTROYED);
 
             // Verify Activity onDestroy even is reported to renderer.
-            verify(mRenderService, times(1)).terminate(
-                    mFakeCarAppServiceComponent);
+            verify(mRenderService, times(1)).terminate(mFakeCarAppServiceComponent);
             // Verify service connection is closed.
-            verify(serviceConnection, times(1)).onServiceDisconnected(
-                    mRendererComponent);
+            verify(serviceConnection, times(1)).onServiceDisconnected(mRendererComponent);
             assertThat(serviceConnectionManager.isBound()).isFalse();
 
         });
@@ -223,8 +220,7 @@
             IRendererCallback rendererCallback = mock(IRendererCallback.class);
 
             ICarAppActivity carAppActivity = mRenderServiceDelegate.getCarAppActivity();
-            carAppActivity.setSurfacePackage(
-                    Bundleable.create(new LegacySurfacePackage(callback)));
+            carAppActivity.setSurfacePackage(Bundleable.create(new LegacySurfacePackage(callback)));
             carAppActivity.registerRendererCallback(rendererCallback);
 
             // Verify back events on the activity are sent to host.
@@ -243,11 +239,9 @@
             int x = 50;
             int y = 50;
             int metaState = 0;
-            MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y,
-                    metaState);
+            MotionEvent event = MotionEvent.obtain(downTime, eventTime, action, x, y, metaState);
             activity.mSurfaceView.dispatchTouchEvent(event);
-            ArgumentCaptor argument = ArgumentCaptor.forClass(
-                    MotionEvent.class);
+            ArgumentCaptor argument = ArgumentCaptor.forClass(MotionEvent.class);
             verify(callback, times(1)).onTouchEvent(argument.capture());
             // Compare string representations as equals in MotionEvent checks for same
             // object.
@@ -306,15 +300,13 @@
             View activityContainer = activity.mActivityContainerView;
             View localContentContainer = activity.mLocalContentContainerView;
             Insets systemWindowInsets = Insets.of(10, 20, 30, 40);
-            WindowInsets windowInsets = new WindowInsetsCompat.Builder()
-                    .setInsets(WindowInsetsCompat.Type.systemBars(), systemWindowInsets)
-                    .build()
-                    .toWindowInsets();
+            WindowInsets windowInsets = new WindowInsetsCompat.Builder().setInsets(
+                    WindowInsetsCompat.Type.systemBars(),
+                    systemWindowInsets).build().toWindowInsets();
             activityContainer.onApplyWindowInsets(windowInsets);
 
             // Verify that the host is notified and insets are not handled locally
-            verify(insetsListener).onInsetsChanged(
-                    eq(systemWindowInsets.toPlatformInsets()));
+            verify(insetsListener).onInsetsChanged(eq(systemWindowInsets.toPlatformInsets()));
             assertThat(activityContainer.getPaddingBottom()).isEqualTo(0);
             assertThat(activityContainer.getPaddingTop()).isEqualTo(0);
             assertThat(activityContainer.getPaddingLeft()).isEqualTo(0);
@@ -346,6 +338,62 @@
         });
     }
 
+    @Test
+    public void testLaunchWithIdentifier_passesAlongValues() {
+        ArgumentCaptor intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
+        Intent newIntent = new Intent(getApplicationContext(), CarAppActivity.class);
+        newIntent.setIdentifier(INTENT_IDENTIFIER);
+        try (ActivityScenario scenario = ActivityScenario.launch(newIntent)) {
+            scenario.onActivity(activity -> {
+                try {
+                    verify(mRenderService, times(1)).initialize(
+                            mRenderServiceDelegate.getCarAppActivity(), mFakeCarAppServiceComponent,
+                            activity.getDisplayId());
+                    verify(mRenderService, times(1)).onNewIntent(intentArgumentCaptor.capture(),
+                            eq(mFakeCarAppServiceComponent), eq(activity.getDisplayId()));
+
+                    Intent intent = intentArgumentCaptor.getValue();
+                    SessionInfo si = new SessionInfo(intent);
+
+                    assertThat(si.getDisplayType()).isEqualTo(SessionInfo.DISPLAY_TYPE_MAIN);
+                    assertThat(intent.getIdentifier()).isEqualTo(si.toString());
+                } catch (Exception e) {
+                    fail(Log.getStackTraceString(e));
+                }
+            });
+
+        }
+
+    }
+
+    @Test
+    public void testLaunchWithoutIdentifier_setsRandomIdValue() {
+        ArgumentCaptor intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
+        Intent newIntent = new Intent(getApplicationContext(), CarAppActivity.class);
+        try (ActivityScenario scenario = ActivityScenario.launch(newIntent)) {
+            scenario.onActivity(activity -> {
+                try {
+                    verify(mRenderService, times(1)).initialize(
+                            mRenderServiceDelegate.getCarAppActivity(), mFakeCarAppServiceComponent,
+                            activity.getDisplayId());
+                    verify(mRenderService, times(1)).onNewIntent(intentArgumentCaptor.capture(),
+                            eq(mFakeCarAppServiceComponent), eq(activity.getDisplayId()));
+
+                    Intent intent = intentArgumentCaptor.getValue();
+                    SessionInfo si = new SessionInfo(intent);
+
+                    assertThat(si.getDisplayType()).isEqualTo(SessionInfo.DISPLAY_TYPE_MAIN);
+                    assertThat(intent.getIdentifier()).isNotEqualTo(INTENT_IDENTIFIER);
+                    assertThat(intent.getIdentifier()).isEqualTo(si.toString());
+                } catch (Exception e) {
+                    fail(Log.getStackTraceString(e));
+                }
+            });
+
+        }
+
+    }
+
     interface CarActivityAction {
         void accept(ActivityScenario scenario, CarAppActivity activity)
                 throws Exception;
@@ -361,6 +409,7 @@
                     fail(Log.getStackTraceString(e));
                 }
             });
+
         }
     }
 }
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelFactoryTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelFactoryTest.java
index 7650a2d..8625a22 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelFactoryTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelFactoryTest.java
@@ -16,6 +16,8 @@
 
 package androidx.car.app.activity;
 
+import static androidx.car.app.SessionInfo.DEFAULT_SESSION_INFO;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import android.app.Application;
@@ -42,10 +44,10 @@
     @Test
     public void getInstance_sameKey_returnsSame() {
         CarAppViewModelFactory factory1 = CarAppViewModelFactory.getInstance(mApplication,
-                TEST_COMPONENT_NAME_1);
+                TEST_COMPONENT_NAME_1, DEFAULT_SESSION_INFO);
 
         CarAppViewModelFactory factory2 = CarAppViewModelFactory.getInstance(mApplication,
-                TEST_COMPONENT_NAME_1);
+                TEST_COMPONENT_NAME_1, DEFAULT_SESSION_INFO);
 
         assertThat(factory1).isEqualTo(factory2);
     }
@@ -53,10 +55,10 @@
     @Test
     public void getInstance_differentKeys_returnsDifferent() {
         CarAppViewModelFactory factory1 = CarAppViewModelFactory.getInstance(mApplication,
-                TEST_COMPONENT_NAME_1);
+                TEST_COMPONENT_NAME_1, DEFAULT_SESSION_INFO);
 
         CarAppViewModelFactory factory2 = CarAppViewModelFactory.getInstance(mApplication,
-                TEST_COMPONENT_NAME_2);
+                TEST_COMPONENT_NAME_2, DEFAULT_SESSION_INFO);
 
         assertThat(factory1).isNotEqualTo(factory2);
     }
@@ -64,7 +66,7 @@
     @Test
     public void create_correctComponentName() {
         CarAppViewModelFactory factory = CarAppViewModelFactory.getInstance(mApplication,
-                TEST_COMPONENT_NAME_1);
+                TEST_COMPONENT_NAME_1, DEFAULT_SESSION_INFO);
 
         CarAppViewModel viewModel = factory.create(CarAppViewModel.class);
 
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelTest.java
index 3c7741f3..353977c 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppViewModelTest.java
@@ -16,6 +16,8 @@
 
 package androidx.car.app.activity;
 
+import static androidx.car.app.SessionInfo.DEFAULT_SESSION_INFO;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.mock;
@@ -39,7 +41,7 @@
 
 /** Tests for {@link CarAppViewModel} */
 @RunWith(RobolectricTestRunner.class)
-@Config(instrumentedPackages = { "androidx.car.app.activity" })
+@Config(instrumentedPackages = {"androidx.car.app.activity"})
 @DoNotInstrument
 public class CarAppViewModelTest {
     private static final ComponentName TEST_COMPONENT_NAME = new ComponentName(
@@ -49,7 +51,7 @@
 
     private final Application mApplication = ApplicationProvider.getApplicationContext();
     private CarAppViewModel mCarAppViewModel;
-    private CarAppActivity mCarAppActivity = mock(CarAppActivity.class);
+    private final CarAppActivity mCarAppActivity = mock(CarAppActivity.class);
     private ICarAppActivity mICarAppActivity;
     private ShadowLooper mMainLooper;
     private final ServiceConnectionManager mServiceConnectionManager =
@@ -57,7 +59,8 @@
 
     @Before
     public void setUp() {
-        mCarAppViewModel = new CarAppViewModel(mApplication, TEST_COMPONENT_NAME);
+        mCarAppViewModel = new CarAppViewModel(mApplication, TEST_COMPONENT_NAME,
+                DEFAULT_SESSION_INFO);
         mCarAppViewModel.setActivity(mCarAppActivity);
         mICarAppActivity = mock(ICarAppActivity.class);
         mMainLooper = shadowOf(mApplication.getMainLooper());
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/activity/ResultManagerAutomotiveTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/activity/ResultManagerAutomotiveTest.java
index 6f987f8..ca31699 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/activity/ResultManagerAutomotiveTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/activity/ResultManagerAutomotiveTest.java
@@ -16,6 +16,8 @@
 
 package androidx.car.app.activity;
 
+import static androidx.car.app.SessionInfo.DEFAULT_SESSION_INFO;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -43,19 +45,20 @@
 
 /** Tests for {@link ResultManagerAutomotive}. */
 @RunWith(RobolectricTestRunner.class)
-@Config(instrumentedPackages = { "androidx.car.app.activity" })
+@Config(instrumentedPackages = {"androidx.car.app.activity"})
 @DoNotInstrument
 public class ResultManagerAutomotiveTest {
     private final ComponentName mRendererComponent = new ComponentName(
             ApplicationProvider.getApplicationContext(), getClass().getName());
     private final Application mApplication = ApplicationProvider.getApplicationContext();
-    private CarAppActivity mCarAppActivity = mock(CarAppActivity.class);
-    private ResultManagerAutomotive mResultManager = new ResultManagerAutomotive();
+    private final CarAppActivity mCarAppActivity = mock(CarAppActivity.class);
+    private final ResultManagerAutomotive mResultManager = new ResultManagerAutomotive();
     private CarAppViewModel mCarAppViewModel;
 
     @Before
     public void setUp() {
-        mCarAppViewModel = new CarAppViewModel(mApplication, mRendererComponent);
+        mCarAppViewModel = new CarAppViewModel(mApplication, mRendererComponent,
+                DEFAULT_SESSION_INFO);
     }
 
     @Test
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/activity/ServiceConnectionManagerTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/activity/ServiceConnectionManagerTest.java
index 5bfc022..72c040b 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/activity/ServiceConnectionManagerTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/activity/ServiceConnectionManagerTest.java
@@ -18,11 +18,14 @@
 
 import static android.os.Looper.getMainLooper;
 
+import static androidx.car.app.SessionInfo.DEFAULT_SESSION_INFO;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -38,6 +41,7 @@
 import android.util.Log;
 
 import androidx.car.app.HandshakeInfo;
+import androidx.car.app.SessionInfo;
 import androidx.car.app.activity.renderer.ICarAppActivity;
 import androidx.car.app.activity.renderer.IRendererService;
 import androidx.car.app.serialization.Bundleable;
@@ -47,6 +51,7 @@
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.internal.DoNotInstrument;
 import org.robolectric.shadows.ShadowApplication;
@@ -72,7 +77,7 @@
             new RenderServiceDelegate(mRenderService);
     private final CarAppViewModel mViewModel =
             new CarAppViewModel(ApplicationProvider.getApplicationContext(),
-                    mFakeCarAppServiceComponent);
+                    mFakeCarAppServiceComponent, DEFAULT_SESSION_INFO);
     private final ServiceConnectionManager mServiceConnectionManager =
             mViewModel.getServiceConnectionManager();
     private final ShadowLooper mMainLooper = shadowOf(getMainLooper());
@@ -125,7 +130,7 @@
     public void testBind_unbound_bindsToRenderer() {
         setupCarAppActivityForTesting();
         ICarAppActivity iCarAppActivity = mock(ICarAppActivity.class);
-
+        ArgumentCaptor intentArgumentCaptor = ArgumentCaptor.forClass(Intent.class);
         mServiceConnectionManager.bind(TEST_INTENT, iCarAppActivity, TEST_DISPLAY_ID);
         mMainLooper.idle();
         try {
@@ -133,12 +138,17 @@
             assertThat(mViewModel.getError().getValue()).isNull();
             verify(mRenderService).initialize(iCarAppActivity, mFakeCarAppServiceComponent,
                     TEST_DISPLAY_ID);
-            verify(mRenderService).onNewIntent(TEST_INTENT, mFakeCarAppServiceComponent,
-                    TEST_DISPLAY_ID);
+            verify(mRenderService).onNewIntent(intentArgumentCaptor.capture(),
+                    eq(mFakeCarAppServiceComponent),
+                    eq(TEST_DISPLAY_ID));
         } catch (RemoteException e) {
             fail(Log.getStackTraceString(e));
         }
         assertThat(mServiceConnectionManager.isBound()).isTrue();
+        assertThat(intentArgumentCaptor.getValue()).isEqualTo(TEST_INTENT);
+        assertThat(intentArgumentCaptor.getValue().getExtras()).isNotNull();
+        assertThat(new SessionInfo(intentArgumentCaptor.getValue())).isEqualTo(
+                DEFAULT_SESSION_INFO);
     }
 
     @Test
diff --git a/car/app/app-samples/helloworld/automotive/github_build.gradle b/car/app/app-samples/helloworld/automotive/github_build.gradle
index 2e30e16..7fe19aa 100644
--- a/car/app/app-samples/helloworld/automotive/github_build.gradle
+++ b/car/app/app-samples/helloworld/automotive/github_build.gradle
@@ -17,12 +17,12 @@
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 31
+    compileSdk 32
 
     defaultConfig {
         applicationId "androidx.car.app.sample.helloworld"
         minSdkVersion 29
-        targetSdkVersion 31
+        targetSdkVersion 32
         versionCode 1
         versionName "1.0"
     }
@@ -41,6 +41,7 @@
         targetCompatibility = JavaVersion.VERSION_1_8
         sourceCompatibility = JavaVersion.VERSION_1_8
     }
+    namespace "androidx.car.app.sample.helloworld"
 }
 
 dependencies {
diff --git a/car/app/app-samples/helloworld/common/github_build.gradle b/car/app/app-samples/helloworld/common/github_build.gradle
index 8eb82ea..e5cf456 100644
--- a/car/app/app-samples/helloworld/common/github_build.gradle
+++ b/car/app/app-samples/helloworld/common/github_build.gradle
@@ -17,11 +17,11 @@
 apply plugin: 'com.android.library'
 
 android {
-    compileSdkVersion 31
+    compileSdk 32
 
     defaultConfig {
         minSdkVersion 23
-        targetSdkVersion 31
+        targetSdkVersion 32
         versionCode 1
         versionName "1.0"
     }
@@ -40,6 +40,7 @@
         targetCompatibility = JavaVersion.VERSION_1_8
         sourceCompatibility = JavaVersion.VERSION_1_8
     }
+    namespace "androidx.car.app.sample.helloworld.common"
 }
 
 dependencies {
diff --git a/car/app/app-samples/helloworld/mobile/github_build.gradle b/car/app/app-samples/helloworld/mobile/github_build.gradle
index ea2d696..f836662 100644
--- a/car/app/app-samples/helloworld/mobile/github_build.gradle
+++ b/car/app/app-samples/helloworld/mobile/github_build.gradle
@@ -17,12 +17,12 @@
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 31
+    compileSdk 32
 
     defaultConfig {
         applicationId "androidx.car.app.sample.helloworld"
         minSdkVersion 23
-        targetSdkVersion 31
+        targetSdkVersion 32
         versionCode 1
         versionName "1.0"
     }
@@ -41,6 +41,7 @@
         targetCompatibility = JavaVersion.VERSION_1_8
         sourceCompatibility = JavaVersion.VERSION_1_8
     }
+    namespace "androidx.car.app.sample.helloworld"
 }
 
 dependencies {
diff --git a/car/app/app-samples/navigation/automotive/github_build.gradle b/car/app/app-samples/navigation/automotive/github_build.gradle
index 208026e..c794e97 100644
--- a/car/app/app-samples/navigation/automotive/github_build.gradle
+++ b/car/app/app-samples/navigation/automotive/github_build.gradle
@@ -17,12 +17,12 @@
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 31
+    compileSdk 32
 
     defaultConfig {
         applicationId "androidx.car.app.sample.navigation"
         minSdkVersion 29
-        targetSdkVersion 31
+        targetSdkVersion 32
         // Increment this to generate signed builds for uploading to Playstore
         // Make sure this is different from the navigation-mobile version
         versionCode 110
@@ -43,6 +43,7 @@
         targetCompatibility = JavaVersion.VERSION_1_8
         sourceCompatibility = JavaVersion.VERSION_1_8
     }
+    namespace "androidx.car.app.sample.navigation"
 }
 
 dependencies {
diff --git a/car/app/app-samples/navigation/common/github_build.gradle b/car/app/app-samples/navigation/common/github_build.gradle
index 0b13b40..878aa78 100644
--- a/car/app/app-samples/navigation/common/github_build.gradle
+++ b/car/app/app-samples/navigation/common/github_build.gradle
@@ -17,11 +17,11 @@
 apply plugin: 'com.android.library'
 
 android {
-    compileSdkVersion 31
+    compileSdk 32
 
     defaultConfig {
         minSdkVersion 23
-        targetSdkVersion 31
+        targetSdkVersion 32
         versionCode 1
         versionName "1.0"
     }
@@ -29,6 +29,7 @@
         targetCompatibility = JavaVersion.VERSION_1_8
         sourceCompatibility = JavaVersion.VERSION_1_8
     }
+    namespace "androidx.car.app.sample.navigation.common"
 }
 
 dependencies {
diff --git a/car/app/app-samples/navigation/mobile/github_build.gradle b/car/app/app-samples/navigation/mobile/github_build.gradle
index e4aebc6..f65aea6 100644
--- a/car/app/app-samples/navigation/mobile/github_build.gradle
+++ b/car/app/app-samples/navigation/mobile/github_build.gradle
@@ -17,12 +17,12 @@
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 31
+    compileSdk 32
 
     defaultConfig {
         applicationId "androidx.car.app.sample.navigation"
         minSdkVersion 23
-        targetSdkVersion 31
+        targetSdkVersion 32
         // Increment this to generate signed builds for uploading to Playstore
         // Make sure this is different from the navigation-automotive version
         versionCode 110
@@ -43,6 +43,7 @@
         targetCompatibility = JavaVersion.VERSION_1_8
         sourceCompatibility = JavaVersion.VERSION_1_8
     }
+    namespace "androidx.car.app.sample.navigation"
 }
 
 dependencies {
diff --git a/car/app/app-samples/places/automotive/github_build.gradle b/car/app/app-samples/places/automotive/github_build.gradle
index f9b2078..0cade8e 100644
--- a/car/app/app-samples/places/automotive/github_build.gradle
+++ b/car/app/app-samples/places/automotive/github_build.gradle
@@ -17,11 +17,11 @@
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 31
+    compileSdk 32
     defaultConfig {
         applicationId "androidx.car.app.sample.places"
         minSdkVersion 29
-        targetSdkVersion 31
+        targetSdkVersion 32
         versionCode 1
         versionName "1.0"
     }
@@ -40,6 +40,7 @@
         targetCompatibility = JavaVersion.VERSION_1_8
         sourceCompatibility = JavaVersion.VERSION_1_8
     }
+    namespace "androidx.car.app.sample.places"
 }
 
 dependencies {
diff --git a/car/app/app-samples/places/common/github_build.gradle b/car/app/app-samples/places/common/github_build.gradle
index e51f92f..da6b961 100644
--- a/car/app/app-samples/places/common/github_build.gradle
+++ b/car/app/app-samples/places/common/github_build.gradle
@@ -17,10 +17,10 @@
 apply plugin: 'com.android.library'
 
 android {
-    compileSdkVersion 31
+    compileSdk 32
     defaultConfig {
         minSdkVersion 23
-        targetSdkVersion 31
+        targetSdkVersion 32
         versionCode 1
         versionName "1.0"
     }
@@ -28,6 +28,7 @@
         targetCompatibility = JavaVersion.VERSION_1_8
         sourceCompatibility = JavaVersion.VERSION_1_8
     }
+    namespace "androidx.car.app.sample.places.common"
 }
 
 dependencies {
diff --git a/car/app/app-samples/places/mobile/github_build.gradle b/car/app/app-samples/places/mobile/github_build.gradle
index cf37c23..b93b250 100644
--- a/car/app/app-samples/places/mobile/github_build.gradle
+++ b/car/app/app-samples/places/mobile/github_build.gradle
@@ -17,11 +17,11 @@
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 31
+    compileSdk 32
     defaultConfig {
         applicationId "androidx.car.app.sample.places"
         minSdkVersion 23
-        targetSdkVersion 31
+        targetSdkVersion 32
         versionCode 1
         versionName "1.0"
     }
@@ -40,6 +40,7 @@
         targetCompatibility = JavaVersion.VERSION_1_8
         sourceCompatibility = JavaVersion.VERSION_1_8
     }
+    namespace "androidx.car.app.sample.places"
 }
 
 dependencies {
diff --git a/car/app/app-samples/showcase/automotive/github_build.gradle b/car/app/app-samples/showcase/automotive/github_build.gradle
index 4622b8c..fe1253d 100644
--- a/car/app/app-samples/showcase/automotive/github_build.gradle
+++ b/car/app/app-samples/showcase/automotive/github_build.gradle
@@ -17,12 +17,12 @@
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 31
+    compileSdk 32
 
     defaultConfig {
         applicationId "androidx.car.app.sample.showcase"
         minSdkVersion 29
-        targetSdkVersion 31
+        targetSdkVersion 32
         // Increment this to generate signed builds for uploading to Playstore
         // Make sure this is different from the showcase-mobile version
         versionCode 110
@@ -45,6 +45,7 @@
     }
 
     testOptions.unitTests.includeAndroidResources true
+    namespace "androidx.car.app.sample.showcase"
 }
 
 dependencies {
diff --git a/car/app/app-samples/showcase/common/github_build.gradle b/car/app/app-samples/showcase/common/github_build.gradle
index 33a90bb..acb2903 100644
--- a/car/app/app-samples/showcase/common/github_build.gradle
+++ b/car/app/app-samples/showcase/common/github_build.gradle
@@ -17,11 +17,11 @@
 apply plugin: 'com.android.library'
 
 android {
-    compileSdkVersion 31
+    compileSdk 32
 
     defaultConfig {
         minSdkVersion 23
-        targetSdkVersion 31
+        targetSdkVersion 32
         versionCode 1
         versionName "1.0"
     }
@@ -30,6 +30,7 @@
         targetCompatibility = JavaVersion.VERSION_1_8
         sourceCompatibility = JavaVersion.VERSION_1_8
     }
+    namespace "androidx.car.app.sample.showcase.common"
 }
 
 dependencies {
diff --git a/car/app/app-samples/showcase/mobile/github_build.gradle b/car/app/app-samples/showcase/mobile/github_build.gradle
index 01471df..b3a126d 100644
--- a/car/app/app-samples/showcase/mobile/github_build.gradle
+++ b/car/app/app-samples/showcase/mobile/github_build.gradle
@@ -17,12 +17,12 @@
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 31
+    compileSdk 32
 
     defaultConfig {
         applicationId "androidx.car.app.sample.showcase"
         minSdkVersion 23
-        targetSdkVersion 31
+        targetSdkVersion 32
         // Increment this to generate signed builds for uploading to Playstore
         // Make sure this is different from the showcase-automotive version
         versionCode 110
@@ -45,6 +45,7 @@
     }
 
     testOptions.unitTests.includeAndroidResources true
+    namespace "androidx.car.app.sample.showcase"
 }
 
 dependencies {
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 347b66d..a696657a 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -159,11 +159,14 @@
 
   @androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.RequiresCarApi(5) public class SessionInfo {
     ctor public SessionInfo(int, String);
+    ctor public SessionInfo(android.content.Intent);
     method public int getDisplayType();
     method public String getSessionId();
+    method public static void setBindData(android.content.Intent, androidx.car.app.SessionInfo);
     field public static final androidx.car.app.SessionInfo DEFAULT_SESSION_INFO;
     field public static final int DISPLAY_TYPE_CLUSTER = 1; // 0x1
     field public static final int DISPLAY_TYPE_MAIN = 0; // 0x0
+    field public static final String EXTRA_SESSION_INFO = "androidx.car.app.extra.SESSION_INFO";
   }
 
   public interface SurfaceCallback {
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 0556f7b..d69fc33 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -160,12 +160,15 @@
 
   @androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.RequiresCarApi(5) public class SessionInfo {
     ctor public SessionInfo(int, String);
+    ctor public SessionInfo(android.content.Intent);
     method public int getDisplayType();
     method public String getSessionId();
     method @androidx.car.app.annotations.ExperimentalCarApi public java.util.Set!>? getSupportedTemplates(int);
+    method public static void setBindData(android.content.Intent, androidx.car.app.SessionInfo);
     field public static final androidx.car.app.SessionInfo DEFAULT_SESSION_INFO;
     field public static final int DISPLAY_TYPE_CLUSTER = 1; // 0x1
     field public static final int DISPLAY_TYPE_MAIN = 0; // 0x0
+    field public static final String EXTRA_SESSION_INFO = "androidx.car.app.extra.SESSION_INFO";
   }
 
   public interface SurfaceCallback {
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 347b66d..a696657a 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -159,11 +159,14 @@
 
   @androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.RequiresCarApi(5) public class SessionInfo {
     ctor public SessionInfo(int, String);
+    ctor public SessionInfo(android.content.Intent);
     method public int getDisplayType();
     method public String getSessionId();
+    method public static void setBindData(android.content.Intent, androidx.car.app.SessionInfo);
     field public static final androidx.car.app.SessionInfo DEFAULT_SESSION_INFO;
     field public static final int DISPLAY_TYPE_CLUSTER = 1; // 0x1
     field public static final int DISPLAY_TYPE_MAIN = 0; // 0x0
+    field public static final String EXTRA_SESSION_INFO = "androidx.car.app.extra.SESSION_INFO";
   }
 
   public interface SurfaceCallback {
diff --git a/car/app/app/src/main/java/androidx/car/app/SessionInfo.java b/car/app/app/src/main/java/androidx/car/app/SessionInfo.java
index 3176923..8006c178 100644
--- a/car/app/app/src/main/java/androidx/car/app/SessionInfo.java
+++ b/car/app/app/src/main/java/androidx/car/app/SessionInfo.java
@@ -17,15 +17,24 @@
 
 import static java.lang.annotation.RetentionPolicy.SOURCE;
 
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.annotation.DoNotInline;
 import androidx.annotation.IntDef;
 import androidx.annotation.Keep;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.car.app.annotations.CarProtocol;
 import androidx.car.app.annotations.ExperimentalCarApi;
 import androidx.car.app.annotations.RequiresCarApi;
 import androidx.car.app.model.Template;
 import androidx.car.app.navigation.model.NavigationTemplate;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
 import androidx.car.app.versioning.CarAppApiLevel;
 import androidx.car.app.versioning.CarAppApiLevels;
 
@@ -41,6 +50,9 @@
 public class SessionInfo {
     private static final char DIVIDER = '/';
 
+    /** The key for a {@link Bundleable} extra containing the {@link SessionInfo} for a bind. */
+    public static final String EXTRA_SESSION_INFO = "androidx.car.app.extra.SESSION_INFO";
+
     /** The primary infotainment display usually in the center column of the vehicle. */
     public static final int DISPLAY_TYPE_MAIN = 0;
 
@@ -101,6 +113,53 @@
         mSessionId = sessionId;
     }
 
+    /**
+     * Creates a new {@link SessionInfo} for a given bind {@code intent}
+     */
+    @SuppressWarnings("deprecation")
+    public SessionInfo(@NonNull Intent intent) {
+        Bundle extras = intent.getExtras();
+        if (extras == null) {
+            throw new IllegalArgumentException(
+                    "Expected the SessionInfo to be encoded in the bind intent extras, but the "
+                            + "extras were null.");
+        }
+
+        Bundleable sessionInfoBundleable = extras.getParcelable(EXTRA_SESSION_INFO);
+        if (sessionInfoBundleable == null) {
+            throw new IllegalArgumentException(
+                    "Expected the SessionInfo to be encoded in the bind intent extras, but they "
+                            + "couldn't be found in the extras.");
+        }
+
+        try {
+            SessionInfo info = (SessionInfo) sessionInfoBundleable.get();
+            this.mSessionId = info.mSessionId;
+            this.mDisplayType = info.mDisplayType;
+        } catch (BundlerException e) {
+            throw new IllegalArgumentException(
+                    "Expected the SessionInfo to be encoded in the bind intent extras, but they "
+                            + "were encoded improperly", e);
+        }
+    }
+
+    /**
+     * Adds the {@link SessionInfo} and associated identifier on the passed {@code intent} to
+     * pass to this service on bind.
+     */
+    public static void setBindData(@NonNull Intent intent, @NonNull SessionInfo sessionInfo) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            Api29.setIdentifier(intent, sessionInfo.toString());
+        } else {
+            intent.setData(new Uri.Builder().path(sessionInfo.toString()).build());
+        }
+        try {
+            intent.putExtra(EXTRA_SESSION_INFO, Bundleable.create(sessionInfo));
+        } catch (BundlerException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     // Required for Bundler
     private SessionInfo() {
         mSessionId = "main";
@@ -153,4 +212,25 @@
         return this.getSessionId().equals(object.getSessionId())
                 && this.getDisplayType() == object.getDisplayType();
     }
+
+    /** Android Q method calls wrapped in a {@link RequiresApi} class to appease the compiler. */
+    @RequiresApi(Build.VERSION_CODES.Q)
+    private static class Api29 {
+        // Not instantiable
+        private Api29() {
+        }
+
+        /** Wrapper for {@link Intent#getIdentifier()}. */
+        @DoNotInline
+        @Nullable
+        static String getIdentifier(@NonNull Intent intent) {
+            return intent.getIdentifier();
+        }
+
+        /** Wrapper for {@link Intent#setIdentifier(String)}. */
+        @DoNotInline
+        static void setIdentifier(@NonNull Intent intent, @NonNull String identifier) {
+            intent.setIdentifier(identifier);
+        }
+    }
 }
diff --git a/car/app/app/src/test/java/androidx/car/app/CarAppBinderTest.java b/car/app/app/src/test/java/androidx/car/app/CarAppBinderTest.java
index 63c5c9c..f21bd8d 100644
--- a/car/app/app/src/test/java/androidx/car/app/CarAppBinderTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/CarAppBinderTest.java
@@ -51,6 +51,7 @@
 import androidx.test.core.app.ApplicationProvider;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -196,6 +197,7 @@
     }
 
     @SuppressLint("NewApi")
+    @Ignore // b/238635208
     @Test
     public void onAppCreate_updatesTheConfiguration() {
         Configuration configuration = new Configuration();
@@ -250,6 +252,7 @@
     }
 
     @SuppressLint("NewApi")
+    @Ignore // b/238635208
     @Test
     public void onConfigurationChanged_updatesTheConfiguration() throws RemoteException {
         mCarAppBinder.onAppCreate(mMockCarHost, null, new Configuration(),
diff --git a/car/app/app/src/test/java/androidx/car/app/CarContextTest.java b/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
index 3ea8abb..bb49c17 100644
--- a/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/CarContextTest.java
@@ -55,6 +55,7 @@
 import androidx.test.core.app.ApplicationProvider;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -260,6 +261,7 @@
         assertThat(name).isNull();
     }
 
+    @Ignore // b/238635208
     @Test
     public void onConfigurationChanged_updatesTheConfiguration() {
         Configuration configuration = new Configuration();
diff --git a/car/app/app/src/test/java/androidx/car/app/SessionInfoTest.java b/car/app/app/src/test/java/androidx/car/app/SessionInfoTest.java
index 3712ac8..1c3a502 100644
--- a/car/app/app/src/test/java/androidx/car/app/SessionInfoTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/SessionInfoTest.java
@@ -16,17 +16,24 @@
 
 package androidx.car.app;
 
+import static androidx.car.app.SessionInfo.DEFAULT_SESSION_INFO;
 import static androidx.car.app.SessionInfo.DISPLAY_TYPE_CLUSTER;
 import static androidx.car.app.SessionInfo.DISPLAY_TYPE_MAIN;
+import static androidx.car.app.SessionInfo.EXTRA_SESSION_INFO;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.os.Build;
+
 import androidx.car.app.model.Template;
 import androidx.car.app.versioning.CarAppApiLevels;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
 import org.robolectric.annotation.internal.DoNotInstrument;
 
 import java.util.Set;
@@ -122,4 +129,50 @@
                 anotherDisplayTypeMainSessionId.toString());
     }
 
+    @Test
+    public void bind_insertsExtra() {
+        Intent intent = new Intent();
+        SessionInfo.setBindData(intent, DEFAULT_SESSION_INFO);
+
+        assertThat(intent.hasExtra(EXTRA_SESSION_INFO));
+    }
+
+    @Test
+    public void bind_from_returnsExpectedValue() {
+        Intent intent = new Intent();
+        SessionInfo.setBindData(intent, DEFAULT_SESSION_INFO);
+
+        SessionInfo info = new SessionInfo(intent);
+
+        assertThat(info).isEqualTo(DEFAULT_SESSION_INFO);
+
+    }
+
+    @SuppressLint("NewApi")
+    @Test
+    @Config(sdk = Build.VERSION_CODES.Q)
+    public void setBindData() {
+        SessionInfo testSessionInfo =
+                new SessionInfo(DISPLAY_TYPE_CLUSTER, "a-unique-session-id");
+        Intent intent = new Intent();
+        SessionInfo.setBindData(intent, testSessionInfo);
+
+        SessionInfo resultSessionInfo = new SessionInfo(intent);
+        assertThat(resultSessionInfo).isEqualTo(testSessionInfo);
+        assertThat(intent.getIdentifier()).isEqualTo(testSessionInfo.toString());
+    }
+
+    @Test
+    @Config(sdk = Build.VERSION_CODES.P)
+    public void setBindData_belowQ() {
+        SessionInfo testSessionInfo =
+                new SessionInfo(DISPLAY_TYPE_CLUSTER, "a-unique-session-id");
+        Intent intent = new Intent();
+        SessionInfo.setBindData(intent, testSessionInfo);
+
+        SessionInfo resultSessionInfo = new SessionInfo(intent);
+        assertThat(resultSessionInfo).isEqualTo(testSessionInfo);
+        assertThat(intent.getDataString()).isEqualTo(testSessionInfo.toString());
+    }
+
 }
diff --git a/car/app/app/src/test/java/androidx/car/app/notification/CarNotificationManagerTest.java b/car/app/app/src/test/java/androidx/car/app/notification/CarNotificationManagerTest.java
index 7e55652..46ae9a4 100644
--- a/car/app/app/src/test/java/androidx/car/app/notification/CarNotificationManagerTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/notification/CarNotificationManagerTest.java
@@ -39,6 +39,7 @@
 import androidx.test.core.app.ApplicationProvider;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
@@ -94,6 +95,7 @@
                 mDefaultPendingIntent);
     }
 
+    @Ignore // b/238635208
     @SuppressWarnings("deprecation")
     @Test
     public void updateForCar_embedded_overridesAsExpected() {
@@ -122,6 +124,7 @@
         assertThat(action.title).isEqualTo(EXTENDED_ACTION_TITLE);
     }
 
+    @Ignore // b/238635208
     @SuppressWarnings("deprecation")
     @Test
     public void updateForCar_notEmbedded_returnsTheSame() {
@@ -147,6 +150,7 @@
         assertThat(action.title).isEqualTo(DEFAULT_ACTION_TITLE);
     }
 
+    @Ignore // b/238635208
     @SuppressWarnings("deprecation")
     @Test
     public void updateForCar_notExtended_marksAsExtended()
diff --git a/collection/collection/build.gradle b/collection/collection/build.gradle
index af379cf..784367d 100644
--- a/collection/collection/build.gradle
+++ b/collection/collection/build.gradle
@@ -36,6 +36,7 @@
     macosX64()
     macosArm64()
     linuxX64()
+    ios()
 
     sourceSets {
         commonMain {
@@ -71,7 +72,8 @@
         targets.all { target ->
             if (target.platformType == KotlinPlatformType.native) {
                 target.compilations["main"].defaultSourceSet {
-                    if (target.konanTarget.family == Family.OSX) {
+                    if (target.konanTarget.family == Family.OSX ||
+                        target.konanTarget.family == Family.IOS) {
                         dependsOn(darwinMain)
                     } else if (target.konanTarget.family == Family.LINUX) {
                         dependsOn(linuxMain)
@@ -127,6 +129,7 @@
         jvm = Publish.SNAPSHOT_AND_RELEASE
         linux = Publish.SNAPSHOT_AND_RELEASE
         mac = Publish.SNAPSHOT_AND_RELEASE
+        ios = Publish.SNAPSHOT_AND_RELEASE
     }
     mavenGroup = LibraryGroups.COLLECTION
     inceptionYear = "2018"
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
index 44c0a94..ed94995 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
@@ -204,7 +204,7 @@
             project: Project,
             configuration: CompilerConfiguration
         ) {
-            val KOTLIN_VERSION_EXPECTATION = "1.7.0"
+            val KOTLIN_VERSION_EXPECTATION = "1.7.10"
             KotlinCompilerVersion.getVersion()?.let { version ->
                 val suppressKotlinVersionCheck = configuration.get(
                     ComposeConfiguration.SUPPRESS_KOTLIN_VERSION_COMPATIBILITY_CHECK,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index b66d0de..7b54efd 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -631,7 +631,7 @@
         // soon be _more_ visible, so don't scroll.
         if (!containerShrunk) return
 
-        val focusedChild = focusedChild ?: return
+        val focusedChild = focusedChild?.takeIf { it.isAttached } ?: return
         val focusedBounds = coordinates.localBoundingBoxOf(focusedChild, clipBounds = false)
 
         // In order to check if we need to scroll to bring the focused child into view, it's not
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index f66a610..7a57b3e 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -347,28 +347,6 @@
   public final class IncludeFontPaddingHelper_androidKt {
   }
 
-  @androidx.compose.runtime.Stable public interface ListItemColors {
-    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State containerColor(boolean enabled);
-    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State headlineColor(boolean enabled);
-    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State leadingIconColor(boolean enabled);
-    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State overlineColor(boolean enabled);
-    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State supportingColor(boolean enabled);
-    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State trailingIconColor(boolean enabled);
-  }
-
-  public final class ListItemDefaults {
-    method @androidx.compose.runtime.Composable public androidx.compose.material3.ListItemColors colors(optional long containerColor, optional long headlineColor, optional long leadingIconColor, optional long overlineColor, optional long supportingColor, optional long trailingIconColor, optional long disabledHeadlineColor, optional long disabledLeadingIconColor, optional long disabledTrailingIconColor);
-    method @androidx.compose.runtime.Composable public long getContainerColor();
-    method @androidx.compose.runtime.Composable public long getContentColor();
-    method public float getElevation();
-    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
-    property @androidx.compose.runtime.Composable public final long ContentColor;
-    property public final float Elevation;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
-    field public static final androidx.compose.material3.ListItemDefaults INSTANCE;
-  }
-
   public final class ListItemKt {
   }
 
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 84a780d..2a4019a 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -504,7 +504,7 @@
     field public static final androidx.compose.material3.InputChipDefaults INSTANCE;
   }
 
-  @androidx.compose.runtime.Stable public interface ListItemColors {
+  @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Stable public interface ListItemColors {
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State containerColor(boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State headlineColor(boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State leadingIconColor(boolean enabled);
@@ -513,7 +513,7 @@
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State trailingIconColor(boolean enabled);
   }
 
-  public final class ListItemDefaults {
+  @androidx.compose.material3.ExperimentalMaterial3Api public final class ListItemDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material3.ListItemColors colors(optional long containerColor, optional long headlineColor, optional long leadingIconColor, optional long overlineColor, optional long supportingColor, optional long trailingIconColor, optional long disabledHeadlineColor, optional long disabledLeadingIconColor, optional long disabledTrailingIconColor);
     method @androidx.compose.runtime.Composable public long getContainerColor();
     method @androidx.compose.runtime.Composable public long getContentColor();
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index f66a610..7a57b3e 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -347,28 +347,6 @@
   public final class IncludeFontPaddingHelper_androidKt {
   }
 
-  @androidx.compose.runtime.Stable public interface ListItemColors {
-    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State containerColor(boolean enabled);
-    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State headlineColor(boolean enabled);
-    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State leadingIconColor(boolean enabled);
-    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State overlineColor(boolean enabled);
-    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State supportingColor(boolean enabled);
-    method @androidx.compose.runtime.Composable public androidx.compose.runtime.State trailingIconColor(boolean enabled);
-  }
-
-  public final class ListItemDefaults {
-    method @androidx.compose.runtime.Composable public androidx.compose.material3.ListItemColors colors(optional long containerColor, optional long headlineColor, optional long leadingIconColor, optional long overlineColor, optional long supportingColor, optional long trailingIconColor, optional long disabledHeadlineColor, optional long disabledLeadingIconColor, optional long disabledTrailingIconColor);
-    method @androidx.compose.runtime.Composable public long getContainerColor();
-    method @androidx.compose.runtime.Composable public long getContentColor();
-    method public float getElevation();
-    method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getShape();
-    property @androidx.compose.runtime.Composable public final long ContainerColor;
-    property @androidx.compose.runtime.Composable public final long ContentColor;
-    property public final float Elevation;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape Shape;
-    field public static final androidx.compose.material3.ListItemDefaults INSTANCE;
-  }
-
   public final class ListItemKt {
   }
 
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt
index 693ac03..071b52b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ListItem.kt
@@ -358,6 +358,7 @@
 /**
  * Contains the default values used by list items.
  */
+@ExperimentalMaterial3Api
 object ListItemDefaults {
     /** The default shape of a list item */
     val Shape: Shape @Composable get() = ListTokens.ListItemContainerShape.toShape()
@@ -422,6 +423,7 @@
  * - See [ListItemDefaults.colors] for the default colors used in a [ListItem].
  */
 @Stable
+@ExperimentalMaterial3Api
 interface ListItemColors {
 
     /** The container color of this [ListItem] based on enabled state */
@@ -450,6 +452,7 @@
 }
 
 /** Default [ListItemColors] implementation. */
+@ExperimentalMaterial3Api
 @Immutable
 private class DefaultListItemColors(
     private val containerColor: Color,
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SnapshotStateObserverBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SnapshotStateObserverBenchmark.kt
index 7b760be..4fe328d 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SnapshotStateObserverBenchmark.kt
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/SnapshotStateObserverBenchmark.kt
@@ -22,6 +22,7 @@
 import androidx.benchmark.junit4.measureRepeated
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.runtime.snapshots.SnapshotStateObserver
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -46,7 +47,7 @@
     private val doNothing: (Any) -> Unit = { _ -> }
 
     private lateinit var stateObserver: SnapshotStateObserver
-    private val models: List> = List(StateCount) { mutableStateOf(0) }
+    private val models: List> = List(StateCount) { mutableStateOf(0) }
     private val nodes: List = List(ScopeCount) { it }
     private lateinit var random: Random
 
@@ -116,6 +117,40 @@
     }
 
     @Test
+    fun derivedStateObservation() {
+        runOnUiThread {
+            val node = Any()
+            val states = models.take(3)
+            val derivedState = derivedStateOf {
+                states[0].value + states[1].value + states[2].value
+            }
+
+            stateObserver.observeReads(node, doNothing) {
+                // read derived state a few times
+                repeat(10) {
+                    derivedState.value
+                }
+            }
+
+            benchmarkRule.measureRepeated {
+                stateObserver.observeReads(node, doNothing) {
+                    // read derived state a few times
+                    repeat(10) {
+                        derivedState.value
+                    }
+                }
+
+                runWithTimingDisabled {
+                    states.forEach {
+                        it.value += 1
+                    }
+                    Snapshot.sendApplyNotifications()
+                }
+            }
+        }
+    }
+
+    @Test
     fun deeplyNestedModelObservations() {
         assumeTrue(Build.VERSION.SDK_INT != 29)
         runOnUiThread {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index 1b652e8..65a96b1 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -710,7 +710,9 @@
                 // Record derived state dependency mapping
                 if (value is DerivedState<*>) {
                     derivedStates.removeScope(value)
-                    value.dependencies.forEach { dependency ->
+                    for (dependency in value.dependencies) {
+                        // skip over empty objects from dependency array
+                        if (dependency == null) break
                         derivedStates.add(dependency, value)
                     }
                 }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
index ee172c5..4563c63 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
@@ -18,6 +18,7 @@
 @file:JvmMultifileClass
 package androidx.compose.runtime
 
+import androidx.compose.runtime.collection.IdentityArrayMap
 import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentList
 import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentListOf
 import androidx.compose.runtime.snapshots.Snapshot
@@ -50,7 +51,7 @@
      * The [dependencies] list can be used to determine when a [StateObject] appears in the apply
      * observer set, if the state could affect value of this derived state.
      */
-    val dependencies: Set
+    val dependencies: Array
 
     /**
      * Mutation policy that controls how changes are handled after state dependencies update.
@@ -77,7 +78,7 @@
             val Unset = Any()
         }
 
-        var dependencies: HashMap? = null
+        var dependencies: IdentityArrayMap? = null
         var result: Any? = Unset
         var resultHash: Int = 0
 
@@ -99,9 +100,9 @@
             val dependencies = sync { dependencies }
             if (dependencies != null) {
                 notifyObservers(derivedState) {
-                    for ((stateObject, readLevel) in dependencies.entries) {
+                    dependencies.forEach { stateObject, readLevel ->
                         if (readLevel != 1) {
-                            continue
+                            return@forEach
                         }
 
                         if (stateObject is DerivedSnapshotState<*>) {
@@ -140,9 +141,9 @@
             // for correct invalidation later
             if (forceDependencyReads) {
                 notifyObservers(this) {
-                    val dependencies = readable.dependencies ?: emptyMap()
+                    val dependencies = readable.dependencies
                     val invalidationNestedLevel = calculationBlockNestedLevel.get() ?: 0
-                    for ((dependency, nestedLevel) in dependencies) {
+                    dependencies?.forEach { dependency, nestedLevel ->
                         calculationBlockNestedLevel.set(nestedLevel + invalidationNestedLevel)
                         snapshot.readObserver?.invoke(dependency)
                     }
@@ -153,7 +154,7 @@
         }
         val nestedCalculationLevel = calculationBlockNestedLevel.get() ?: 0
 
-        val newDependencies = HashMap()
+        val newDependencies = IdentityArrayMap()
         val result = notifyObservers(this) {
             calculationBlockNestedLevel.set(nestedCalculationLevel + 1)
 
@@ -218,18 +219,23 @@
             // value is used instead which doesn't notify. This allow the read observer to read the
             // value and only update the cache once.
             Snapshot.current.readObserver?.invoke(this)
-            return currentValue
+            return first.withCurrent {
+                @Suppress("UNCHECKED_CAST")
+                currentRecord(it, Snapshot.current, true, calculation).result as T
+            }
         }
 
     override val currentValue: T
         get() = first.withCurrent {
             @Suppress("UNCHECKED_CAST")
-            currentRecord(it, Snapshot.current, true, calculation).result as T
+            currentRecord(it, Snapshot.current, false, calculation).result as T
         }
 
-    override val dependencies: Set
+    override val dependencies: Array
         get() = first.withCurrent {
-            currentRecord(it, Snapshot.current, false, calculation).dependencies?.keys ?: emptySet()
+            val record = currentRecord(it, Snapshot.current, false, calculation)
+            @Suppress("UNCHECKED_CAST")
+            record.dependencies?.keys ?: emptyArray()
         }
 
     override fun toString(): String = first.withCurrent {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
index 55240ef..86dfb6b 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
@@ -17,6 +17,7 @@
 package androidx.compose.runtime.snapshots
 
 import androidx.compose.runtime.DerivedState
+import androidx.compose.runtime.State
 import androidx.compose.runtime.TestOnly
 import androidx.compose.runtime.collection.IdentityArrayMap
 import androidx.compose.runtime.collection.IdentityArraySet
@@ -125,8 +126,8 @@
             scopeMap.currentScope = scope
 
             observeDerivedStateRecalculations(
-                start = { scopeMap.deriveStateScopeCount++ },
-                done = { scopeMap.deriveStateScopeCount-- },
+                start = scopeMap.derivedStateEnterObserver,
+                done = scopeMap.derivedStateExitObserver
             ) {
                 Snapshot.observe(readObserver, null, block)
             }
@@ -237,7 +238,22 @@
          */
         var currentScope: Any? = null
 
-        var deriveStateScopeCount = 0
+        /**
+         * Start observer for derived state recalculation
+         */
+        val derivedStateEnterObserver: (State<*>) -> Unit = { deriveStateScopeCount++ }
+
+        /**
+         * Exit observer for derived state recalculation
+         */
+        val derivedStateExitObserver: (State<*>) -> Unit = { deriveStateScopeCount-- }
+
+        /**
+         * Counter for skipping reads inside derived states. If count is > 0, read happens inside
+         * a derived state.
+         * Reads for derived states are captured separately through [DerivedState.dependencies].
+         */
+        private var deriveStateScopeCount = 0
 
         /**
          * Values that have been read during the scope's [SnapshotStateObserver.observeReads].
@@ -255,9 +271,15 @@
          */
         private val invalidated = hashSetOf()
 
+        /**
+         * Invalidation index from state objects to derived states reading them.
+         */
         private val dependencyToDerivedStates = IdentityScopeMap>()
 
-        private val derivedStateToValue = HashMap, Any?>()
+        /**
+         * Last derived state value recorded during read.
+         */
+        private val recordedDerivedStateValues = HashMap, Any?>()
 
         /**
          * Record that [value] was read in [currentScope].
@@ -276,12 +298,13 @@
             recordedValues.add(value)
 
             if (value is DerivedState<*>) {
-                dependencyToDerivedStates.removeScope(value)
                 val dependencies = value.dependencies
                 for (dependency in dependencies) {
+                    // skip over dependency array
+                    if (dependency == null) break
                     dependencyToDerivedStates.add(dependency, value)
                 }
-                derivedStateToValue[value] = value.currentValue
+                recordedDerivedStateValues[value] = value.currentValue
             }
         }
 
@@ -293,7 +316,9 @@
             recordedValues.fastForEach {
                 removeObservation(scope, it)
             }
-            scopeToValues.remove(scope)
+            // clearing the scope usually means that we are about to start observation again
+            // so it doesn't make sense to reallocate the set
+            recordedValues.clear()
         }
 
         /**
@@ -313,8 +338,9 @@
 
         private fun removeObservation(scope: Any, value: Any) {
             valueToScopes.remove(value, scope)
-            if (value is DerivedState<*>) {
+            if (value is DerivedState<*> && value !in valueToScopes) {
                 dependencyToDerivedStates.removeScope(value)
+                recordedDerivedStateValues.remove(value)
             }
         }
 
@@ -325,6 +351,7 @@
             valueToScopes.clear()
             scopeToValues.clear()
             dependencyToDerivedStates.clear()
+            recordedDerivedStateValues.clear()
         }
 
         /**
@@ -338,7 +365,7 @@
                     // Find derived state that is invalidated by this change
                     dependencyToDerivedStates.forEachScopeOf(value) { derivedState ->
                         derivedState as DerivedState
-                        val previousValue = derivedStateToValue[derivedState]
+                        val previousValue = recordedDerivedStateValues[derivedState]
                         val policy = derivedState.policy ?: structuralEqualityPolicy()
 
                         // Invalidate only if currentValue is different than observed on read
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsCommon.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsCommon.kt
index 6b9bd48..efc63b90 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsCommon.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserverTestsCommon.kt
@@ -542,6 +542,32 @@
         assertEquals(1, changes)
     }
 
+    @Test
+    fun readingDerivedStateConditionallyInvalidatesBothScopes() {
+        var changes = 0
+
+        runSimpleTest { stateObserver, state ->
+            val derivedState = derivedStateOf { state.value }
+
+            val onChange: (String) -> Unit = { changes++ }
+            stateObserver.observeReads("scope", onChange) {
+                // read derived state
+                derivedState.value
+            }
+
+            // read the same state in other scope
+            stateObserver.observeReads("other scope", onChange) {
+                derivedState.value
+            }
+
+            // stop observing state in other scope
+            stateObserver.observeReads("other scope", onChange) {
+                /* no-op */
+            }
+        }
+        assertEquals(1, changes)
+    }
+
     private fun runSimpleTest(
         block: (modelObserver: SnapshotStateObserver, data: MutableState) -> Unit
     ) {
diff --git a/compose/ui/ui-geometry/api/current.txt b/compose/ui/ui-geometry/api/current.txt
index 936778a..583e373 100644
--- a/compose/ui/ui-geometry/api/current.txt
+++ b/compose/ui/ui-geometry/api/current.txt
@@ -32,7 +32,7 @@
 
   public final class MutableRect {
     ctor public MutableRect(float left, float top, float right, float bottom);
-    method public boolean contains(long offset);
+    method public operator boolean contains(long offset);
     method public float getBottom();
     method public inline float getHeight();
     method public float getLeft();
@@ -105,7 +105,7 @@
     method public float component2();
     method public float component3();
     method public float component4();
-    method public boolean contains(long offset);
+    method public operator boolean contains(long offset);
     method public androidx.compose.ui.geometry.Rect copy(float left, float top, float right, float bottom);
     method @androidx.compose.runtime.Stable public androidx.compose.ui.geometry.Rect deflate(float delta);
     method public float getBottom();
@@ -180,7 +180,7 @@
     method public long component6-kKHJgLs();
     method public long component7-kKHJgLs();
     method public long component8-kKHJgLs();
-    method public boolean contains(long point);
+    method public operator boolean contains(long point);
     method public androidx.compose.ui.geometry.RoundRect copy-MDFrsts(float left, float top, float right, float bottom, long topLeftCornerRadius, long topRightCornerRadius, long bottomRightCornerRadius, long bottomLeftCornerRadius);
     method public float getBottom();
     method public long getBottomLeftCornerRadius();
diff --git a/compose/ui/ui-geometry/api/public_plus_experimental_current.txt b/compose/ui/ui-geometry/api/public_plus_experimental_current.txt
index 936778a..583e373 100644
--- a/compose/ui/ui-geometry/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-geometry/api/public_plus_experimental_current.txt
@@ -32,7 +32,7 @@
 
   public final class MutableRect {
     ctor public MutableRect(float left, float top, float right, float bottom);
-    method public boolean contains(long offset);
+    method public operator boolean contains(long offset);
     method public float getBottom();
     method public inline float getHeight();
     method public float getLeft();
@@ -105,7 +105,7 @@
     method public float component2();
     method public float component3();
     method public float component4();
-    method public boolean contains(long offset);
+    method public operator boolean contains(long offset);
     method public androidx.compose.ui.geometry.Rect copy(float left, float top, float right, float bottom);
     method @androidx.compose.runtime.Stable public androidx.compose.ui.geometry.Rect deflate(float delta);
     method public float getBottom();
@@ -180,7 +180,7 @@
     method public long component6-kKHJgLs();
     method public long component7-kKHJgLs();
     method public long component8-kKHJgLs();
-    method public boolean contains(long point);
+    method public operator boolean contains(long point);
     method public androidx.compose.ui.geometry.RoundRect copy-MDFrsts(float left, float top, float right, float bottom, long topLeftCornerRadius, long topRightCornerRadius, long bottomRightCornerRadius, long bottomLeftCornerRadius);
     method public float getBottom();
     method public long getBottomLeftCornerRadius();
diff --git a/compose/ui/ui-geometry/api/restricted_current.txt b/compose/ui/ui-geometry/api/restricted_current.txt
index 936778a..583e373 100644
--- a/compose/ui/ui-geometry/api/restricted_current.txt
+++ b/compose/ui/ui-geometry/api/restricted_current.txt
@@ -32,7 +32,7 @@
 
   public final class MutableRect {
     ctor public MutableRect(float left, float top, float right, float bottom);
-    method public boolean contains(long offset);
+    method public operator boolean contains(long offset);
     method public float getBottom();
     method public inline float getHeight();
     method public float getLeft();
@@ -105,7 +105,7 @@
     method public float component2();
     method public float component3();
     method public float component4();
-    method public boolean contains(long offset);
+    method public operator boolean contains(long offset);
     method public androidx.compose.ui.geometry.Rect copy(float left, float top, float right, float bottom);
     method @androidx.compose.runtime.Stable public androidx.compose.ui.geometry.Rect deflate(float delta);
     method public float getBottom();
@@ -180,7 +180,7 @@
     method public long component6-kKHJgLs();
     method public long component7-kKHJgLs();
     method public long component8-kKHJgLs();
-    method public boolean contains(long point);
+    method public operator boolean contains(long point);
     method public androidx.compose.ui.geometry.RoundRect copy-MDFrsts(float left, float top, float right, float bottom, long topLeftCornerRadius, long topRightCornerRadius, long bottomRightCornerRadius, long bottomLeftCornerRadius);
     method public float getBottom();
     method public long getBottomLeftCornerRadius();
diff --git a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/MutableRect.kt b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/MutableRect.kt
index b00c587..1e2d5b1 100644
--- a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/MutableRect.kt
+++ b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/MutableRect.kt
@@ -77,7 +77,7 @@
      * Rectangles include their top and left edges but exclude their bottom and
      * right edges.
      */
-    fun contains(offset: Offset): Boolean {
+    operator fun contains(offset: Offset): Boolean {
         return offset.x >= left && offset.x < right && offset.y >= top && offset.y < bottom
     }
 
diff --git a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Rect.kt b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Rect.kt
index 6c976ed..5230b42 100644
--- a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Rect.kt
+++ b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/Rect.kt
@@ -244,7 +244,7 @@
      * Rectangles include their top and left edges but exclude their bottom and
      * right edges.
      */
-    fun contains(offset: Offset): Boolean {
+    operator fun contains(offset: Offset): Boolean {
         return offset.x >= left && offset.x < right && offset.y >= top && offset.y < bottom
     }
 
diff --git a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/RoundRect.kt b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/RoundRect.kt
index 9e1484c..2ce06a3 100644
--- a/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/RoundRect.kt
+++ b/compose/ui/ui-geometry/src/commonMain/kotlin/androidx/compose/ui/geometry/RoundRect.kt
@@ -122,7 +122,7 @@
      * using this method, prefer to reuse existing [RoundRect]s rather than
      * recreating the object each time.
      */
-    fun contains(point: Offset): Boolean {
+    operator fun contains(point: Offset): Boolean {
         if (point.x < left || point.x >= right || point.y < top || point.y >= bottom) {
             return false; // outside bounding box
         }
diff --git a/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/MutableRectTest.kt b/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/MutableRectTest.kt
index 1750492..ce9a9c9 100644
--- a/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/MutableRectTest.kt
+++ b/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/MutableRectTest.kt
@@ -51,18 +51,18 @@
     @Test
     fun contains() {
         val r = MutableRect(1f, 3f, 5f, 9f)
-        assertTrue(r.contains(Offset(1f, 3f)))
-        assertTrue(r.contains(Offset(3f, 3f)))
-        assertFalse(r.contains(Offset(5f, 3f)))
-        assertTrue(r.contains(Offset(1f, 6f)))
-        assertTrue(r.contains(Offset(3f, 6f)))
-        assertFalse(r.contains(Offset(5f, 6f)))
-        assertFalse(r.contains(Offset(1f, 9f)))
-        assertFalse(r.contains(Offset(3f, 9f)))
-        assertFalse(r.contains(Offset(5f, 9f)))
-        assertFalse(r.contains(Offset(0f, 0f)))
-        assertFalse(r.contains(Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)))
-        assertFalse(r.contains(Offset(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY)))
+        assertTrue(Offset(1f, 3f) in r)
+        assertTrue(Offset(3f, 3f) in r)
+        assertFalse(Offset(5f, 3f) in r)
+        assertTrue(Offset(1f, 6f) in r)
+        assertTrue(Offset(3f, 6f) in r)
+        assertFalse(Offset(5f, 6f) in r)
+        assertFalse(Offset(1f, 9f) in r)
+        assertFalse(Offset(3f, 9f) in r)
+        assertFalse(Offset(5f, 9f) in r)
+        assertFalse(Offset(0f, 0f) in r)
+        assertFalse(Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY) in r)
+        assertFalse(Offset(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY) in r)
     }
 
     @Test
diff --git a/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/RectTest.kt b/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/RectTest.kt
index b2e5432..927b408 100644
--- a/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/RectTest.kt
+++ b/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/RectTest.kt
@@ -223,17 +223,17 @@
     fun `rect contains`() {
         val rect = Rect(100f, 10f, 200f, 300f)
         val offset = Offset(177f, 288f)
-        assertTrue(rect.contains(offset))
+        assertTrue(offset in rect)
     }
 
     @Test
     fun `rect does not contain`() {
         val rect = Rect(100f, 10f, 200f, 300f)
         val offset1 = Offset(201f, 150f)
-        assertFalse(rect.contains(offset1))
+        assertFalse(offset1 in rect)
 
         val offset2 = Offset(200f, 301f)
-        assertFalse(rect.contains(offset2))
+        assertFalse(offset2 in rect)
     }
 
     @Test
diff --git a/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/RoundRectTest.kt b/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/RoundRectTest.kt
index 0880e8e..1f6aeac 100644
--- a/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/RoundRectTest.kt
+++ b/compose/ui/ui-geometry/src/test/kotlin/androidx/compose/ui/geometry/RoundRectTest.kt
@@ -36,14 +36,14 @@
             bottomLeft = CornerRadius.Zero
         )
 
-        assertFalse(roundRect.contains(Offset(1.0f, 1.0f)))
-        assertFalse(roundRect.contains(Offset(1.1f, 1.1f)))
-        assertTrue(roundRect.contains(Offset(1.15f, 1.15f)))
-        assertFalse(roundRect.contains(Offset(2.0f, 1.0f)))
-        assertFalse(roundRect.contains(Offset(1.93f, 1.07f)))
-        assertFalse(roundRect.contains(Offset(1.97f, 1.7f)))
-        assertTrue(roundRect.contains(Offset(1.7f, 1.97f)))
-        assertTrue(roundRect.contains(Offset(1.0f, 1.99f)))
+        assertFalse(Offset(1.0f, 1.0f) in roundRect)
+        assertFalse(Offset(1.1f, 1.1f) in roundRect)
+        assertTrue(Offset(1.15f, 1.15f) in roundRect)
+        assertFalse(Offset(2.0f, 1.0f) in roundRect)
+        assertFalse(Offset(1.93f, 1.07f) in roundRect)
+        assertFalse(Offset(1.97f, 1.7f) in roundRect)
+        assertTrue(Offset(1.7f, 1.97f) in roundRect)
+        assertTrue(Offset(1.0f, 1.99f) in roundRect)
     }
 
     @Test
@@ -56,14 +56,14 @@
             bottomLeft = CornerRadius.Zero
         )
 
-        assertFalse(roundRect.contains(Offset(1.0f, 1.0f)))
-        assertFalse(roundRect.contains(Offset(1.1f, 1.1f)))
-        assertTrue(roundRect.contains(Offset(1.15f, 1.15f)))
-        assertFalse(roundRect.contains(Offset(2.0f, 1.0f)))
-        assertFalse(roundRect.contains(Offset(1.93f, 1.07f)))
-        assertFalse(roundRect.contains(Offset(1.97f, 1.7f)))
-        assertTrue(roundRect.contains(Offset(1.7f, 1.97f)))
-        assertTrue(roundRect.contains(Offset(1.0f, 1.99f)))
+        assertFalse(Offset(1.0f, 1.0f) in roundRect)
+        assertFalse(Offset(1.1f, 1.1f) in roundRect)
+        assertTrue(Offset(1.15f, 1.15f) in roundRect)
+        assertFalse(Offset(2.0f, 1.0f) in roundRect)
+        assertFalse(Offset(1.93f, 1.07f) in roundRect)
+        assertFalse(Offset(1.97f, 1.7f) in roundRect)
+        assertTrue(Offset(1.7f, 1.97f) in roundRect)
+        assertTrue(Offset(1.0f, 1.99f) in roundRect)
     }
 
     @Test
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 450d6e1..9dfc07b 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -1739,17 +1739,20 @@
 
   @androidx.compose.runtime.Immutable public final class PointerInputChange {
     ctor public PointerInputChange(long id, long uptimeMillis, long position, boolean pressed, long previousUptimeMillis, long previousPosition, boolean previousPressed, boolean isInitiallyConsumed, optional int type, optional long scrollDelta);
+    ctor @androidx.compose.ui.ExperimentalComposeUiApi public PointerInputChange(long id, long uptimeMillis, long position, boolean pressed, float pressure, long previousUptimeMillis, long previousPosition, boolean previousPressed, boolean isInitiallyConsumed, optional int type, optional long scrollDelta);
     ctor @Deprecated public PointerInputChange(long id, long uptimeMillis, long position, boolean pressed, long previousUptimeMillis, long previousPosition, boolean previousPressed, androidx.compose.ui.input.pointer.ConsumedData consumed, optional int type);
     method public void consume();
     method public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional int type, optional long scrollDelta);
     method @Deprecated public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, androidx.compose.ui.input.pointer.ConsumedData consumed, optional int type, optional long scrollDelta);
     method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional int type, java.util.List historical, optional long scrollDelta);
+    method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.input.pointer.PointerInputChange copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional float pressure, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional int type, java.util.List historical, optional long scrollDelta);
     method @Deprecated public androidx.compose.ui.input.pointer.PointerInputChange! copy(optional long id, optional long currentTime, optional long currentPosition, optional boolean currentPressed, optional long previousTime, optional long previousPosition, optional boolean previousPressed, optional androidx.compose.ui.input.pointer.ConsumedData consumed, optional int type);
     method @Deprecated public androidx.compose.ui.input.pointer.ConsumedData getConsumed();
     method @androidx.compose.ui.ExperimentalComposeUiApi public java.util.List getHistorical();
     method public long getId();
     method public long getPosition();
     method public boolean getPressed();
+    method @androidx.compose.ui.ExperimentalComposeUiApi public float getPressure();
     method public long getPreviousPosition();
     method public boolean getPreviousPressed();
     method public long getPreviousUptimeMillis();
@@ -1763,6 +1766,7 @@
     property public final boolean isConsumed;
     property public final long position;
     property public final boolean pressed;
+    property @androidx.compose.ui.ExperimentalComposeUiApi public final float pressure;
     property public final long previousPosition;
     property public final boolean previousPressed;
     property public final long previousUptimeMillis;
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index 95debb7..db60235 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -40,6 +40,7 @@
 import androidx.compose.ui.demos.focus.TwoDimensionalFocusSearchDemo
 import androidx.compose.ui.demos.gestures.ButtonMetaStateDemo
 import androidx.compose.ui.demos.gestures.DetectTapGesturesDemo
+import androidx.compose.ui.demos.gestures.DetectTapPressureGesturesDemo
 import androidx.compose.ui.demos.gestures.DoubleTapGestureFilterDemo
 import androidx.compose.ui.demos.gestures.DoubleTapInTapDemo
 import androidx.compose.ui.demos.gestures.DragAndScaleGestureFilterDemo
@@ -85,6 +86,7 @@
             listOf(
                 ComposableDemo("Press Indication") { PressIndicatorGestureFilterDemo() },
                 ComposableDemo("Tap") { DetectTapGesturesDemo() },
+                ComposableDemo("Pressure Tap") { DetectTapPressureGesturesDemo() },
                 ComposableDemo("Double Tap") { DoubleTapGestureFilterDemo() },
                 ComposableDemo("Long Press") { LongPressGestureDetectorDemo() },
                 ComposableDemo("Scroll") { ScrollGestureFilterDemo() },
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/TapPressureGestureDetectorDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/TapPressureGestureDetectorDemo.kt
new file mode 100644
index 0000000..4b0f5e4
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/TapPressureGestureDetectorDemo.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.demos.gestures
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+
+/**
+ * Simple demonstration of subscribing to pressure changes.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun DetectTapPressureGesturesDemo() {
+
+    var pressure by remember { mutableStateOf("Press for value") }
+
+    Column(
+        modifier = Modifier.fillMaxSize()
+    ) {
+        Text("The box displays pressure. Tap or use stylus to see different pressure values.")
+        Box(
+            Modifier
+                .fillMaxSize()
+                .padding(20.dp)
+                .wrapContentSize(Alignment.Center)
+                .pointerInput(Unit) {
+                    awaitPointerEventScope {
+                        while (true) {
+                            val event = awaitPointerEvent()
+                            event.changes.forEach {
+                                pressure = "${it.pressure}"
+                                it.consume()
+                            }
+                        }
+                    }
+                }
+                .clipToBounds()
+                .background(Color.Green)
+                .border(BorderStroke(2.dp, BorderColor))
+        ) {
+            Text(
+                modifier = Modifier.fillMaxSize(),
+                textAlign = TextAlign.Center,
+                text = "Pressure: $pressure"
+            )
+        }
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 8ea7c85..5adc1c1 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -220,6 +220,7 @@
         assertTrue(info.isHeading)
         assertTrue(info.isClickable)
         assertTrue(info.isVisibleToUser)
+        assertTrue(info.isImportantForAccessibility)
     }
 
     @Test
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index dafc8ca..442f34c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -144,9 +144,10 @@
             previousEvents += PointerInputEventData(
                 id = PointerId(index.toLong()),
                 uptime = index.toLong(),
-                position = Offset(offset.x + index, offset.y + index),
                 positionOnScreen = Offset(offset.x + index, offset.y + index),
+                position = Offset(offset.x + index, offset.y + index),
                 down = true,
+                pressure = 1.0f,
                 type = pointerType
             )
             val data = previousEvents.map {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt
index 378dbf9..b5821b4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt
@@ -46,6 +46,7 @@
         position,
         position,
         down,
+        pressure = 1.0f,
         PointerType.Touch
     )
 }
@@ -252,12 +253,14 @@
     motionEvent: MotionEvent
 ): InternalPointerEvent {
     val pointers = changes.values.map {
+        @OptIn(ExperimentalComposeUiApi::class)
         PointerInputEventData(
             id = it.id,
             uptime = it.uptimeMillis,
             positionOnScreen = it.position,
             position = it.position,
             down = it.pressed,
+            pressure = it.pressure,
             type = it.type
         )
     }
@@ -330,6 +333,7 @@
             positionOnScreen = it.position,
             position = it.position,
             down = it.pressed,
+            pressure = it.pressure,
             type = it.type,
             issuesEnterExit = false,
             historical = emptyList()
@@ -363,6 +367,7 @@
         positionOnScreen = change.position,
         position = change.position,
         down = change.pressed,
+        pressure = change.pressure,
         type = change.type,
         issuesEnterExit = true,
         historical = emptyList()
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt
index 521c054..e583a5a 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/MotionEventAdapter.android.kt
@@ -258,6 +258,8 @@
 
         val pointerId = getComposePointerId(motionEventPointerId)
 
+        val pressure = motionEvent.getPressure(index)
+
         var position = Offset(motionEvent.getX(index), motionEvent.getY(index))
         val rawPosition: Offset
         if (index == 0) {
@@ -323,6 +325,7 @@
             rawPosition,
             position,
             pressed,
+            pressure,
             toolType,
             issuesEnterExit,
             historical,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt
index df8bb4b..73a1193 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.android.kt
@@ -19,6 +19,7 @@
 import android.view.KeyEvent
 import android.view.MotionEvent
 import android.view.MotionEvent.ACTION_SCROLL
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastMap
 
@@ -83,6 +84,7 @@
     fun component1(): List = changes
 
     // only because PointerEvent was a data class
+    @OptIn(ExperimentalComposeUiApi::class)
     fun copy(
         changes: List,
         motionEvent: MotionEvent?
@@ -101,6 +103,7 @@
                     it.position,
                     it.position,
                     it.pressed,
+                    it.pressure,
                     it.type,
                     this.internalPointerEvent?.issuesEnterExitEvent(it.id) == true
                 )
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 4775e3a..ad153c9 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -431,6 +431,12 @@
 
         info.packageName = view.context.packageName
 
+        // This property exists to distinguish semantically meaningful nodes from purely structural
+        // or decorative UI elements. In Compose, LayoutNodes without semantics are simply omitted
+        // from the AccessibilityNodeInfo tree. Therefore, every AccessibilityNodeInfo qualifies as
+        // "important".
+        info.isImportantForAccessibility = true
+
         semanticsNode.replacedChildrenSortedByBounds.fastForEach { child ->
             if (currentSemanticsNodes.contains(child.id)) {
                 val holder = view.androidViewsHandler.layoutNodeToHolder[child.layoutNode]
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt
index 4a64979..4ead18f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/InternalPointerInput.kt
@@ -45,6 +45,7 @@
     val positionOnScreen: Offset,
     val position: Offset,
     val down: Boolean,
+    val pressure: Float,
     val type: PointerType,
     val issuesEnterExit: Boolean = false,
     val historical: List = mutableListOf(),
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
index 104d8ff0..9eb25aa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
@@ -448,6 +448,7 @@
         uptimeMillis,
         position,
         pressed,
+        pressure = 1.0f,
         previousUptimeMillis,
         previousPosition,
         previousPressed,
@@ -473,6 +474,38 @@
         uptimeMillis,
         position,
         pressed,
+        pressure = 1.0f,
+        previousUptimeMillis,
+        previousPosition,
+        previousPressed,
+        isInitiallyConsumed,
+        type,
+        historical,
+        scrollDelta
+    ) {
+        _historical = historical
+    }
+
+    @ExperimentalComposeUiApi
+    internal constructor(
+        id: PointerId,
+        uptimeMillis: Long,
+        position: Offset,
+        pressed: Boolean,
+        pressure: Float,
+        previousUptimeMillis: Long,
+        previousPosition: Offset,
+        previousPressed: Boolean,
+        isInitiallyConsumed: Boolean,
+        type: PointerType,
+        historical: List,
+        scrollDelta: Offset,
+    ) : this(
+        id,
+        uptimeMillis,
+        position,
+        pressed,
+        pressure,
         previousUptimeMillis,
         previousPosition,
         previousPressed,
@@ -483,6 +516,44 @@
         _historical = historical
     }
 
+    @ExperimentalComposeUiApi
+    constructor(
+        id: PointerId,
+        uptimeMillis: Long,
+        position: Offset,
+        pressed: Boolean,
+        pressure: Float,
+        previousUptimeMillis: Long,
+        previousPosition: Offset,
+        previousPressed: Boolean,
+        isInitiallyConsumed: Boolean,
+        type: PointerType = PointerType.Touch,
+        scrollDelta: Offset = Offset.Zero
+    ) : this (
+        id,
+        uptimeMillis,
+        position,
+        pressed,
+        previousUptimeMillis,
+        previousPosition,
+        previousPressed,
+        isInitiallyConsumed,
+        type,
+        scrollDelta
+    ) {
+        _pressure = pressure
+    }
+
+    /**
+     * Pressure for pointer event
+     */
+    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+    @ExperimentalComposeUiApi
+    @get:ExperimentalComposeUiApi
+    val pressure: Float
+        get() = _pressure ?: 0.0f
+    private var _pressure: Float? = null
+
     /**
      * Optional high-frequency pointer moves in between the last two dispatched events.
      * Can be used for extra accuracy when touchscreen rate exceeds framerate.
@@ -549,6 +620,7 @@
         currentTime,
         currentPosition,
         currentPressed,
+        this.pressure,
         previousTime,
         previousPosition,
         previousPressed,
@@ -584,6 +656,7 @@
         currentTime,
         currentPosition,
         currentPressed,
+        this.pressure,
         previousTime,
         previousPosition,
         previousPressed,
@@ -621,6 +694,7 @@
         currentTime,
         currentPosition,
         currentPressed,
+        this.pressure,
         previousTime,
         previousPosition,
         previousPressed,
@@ -658,6 +732,46 @@
         currentTime,
         currentPosition,
         currentPressed,
+        this.pressure,
+        previousTime,
+        previousPosition,
+        previousPressed,
+        isInitiallyConsumed = false, // doesn't matter, we will pass a holder anyway
+        type,
+        historical,
+        scrollDelta
+    ).also {
+        it.consumed = this.consumed
+    }
+
+    /**
+     * Make a shallow copy of the [PointerInputChange]
+     *
+     * **NOTE:** Due to the need of the inner contract of the [PointerInputChange], this method
+     * performs a shallow copy of the [PointerInputChange]. Any [consume] call between any of the
+     * copies will consume any other copy automatically. Therefore, copy with the new [isConsumed]
+     * is not possible. Consider creating a new [PointerInputChange].
+     */
+    @ExperimentalComposeUiApi
+    @Suppress("DEPRECATION")
+    fun copy(
+        id: PointerId = this.id,
+        currentTime: Long = this.uptimeMillis,
+        currentPosition: Offset = this.position,
+        currentPressed: Boolean = this.pressed,
+        pressure: Float = this.pressure,
+        previousTime: Long = this.previousUptimeMillis,
+        previousPosition: Offset = this.previousPosition,
+        previousPressed: Boolean = this.previousPressed,
+        type: PointerType = this.type,
+        historical: List,
+        scrollDelta: Offset = this.scrollDelta
+    ): PointerInputChange = PointerInputChange(
+        id,
+        currentTime,
+        currentPosition,
+        currentPressed,
+        pressure,
         previousTime,
         previousPosition,
         previousPressed,
@@ -674,6 +788,7 @@
             "uptimeMillis=$uptimeMillis, " +
             "position=$position, " +
             "pressed=$pressed, " +
+            "pressure=$pressure, " +
             "previousUptimeMillis=$previousUptimeMillis, " +
             "previousPosition=$previousPosition, " +
             "previousPressed=$previousPressed, " +
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
index fcc1157..ede8a2e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
@@ -163,6 +163,7 @@
                     it.uptime,
                     it.position,
                     it.down,
+                    it.pressure,
                     previousTime,
                     previousPosition,
                     previousDown,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputTestUtil.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputTestUtil.kt
index 50985c6..a4706cd 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputTestUtil.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputTestUtil.kt
@@ -16,12 +16,14 @@
 
 package androidx.compose.ui.input.pointer
 
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.util.fastForEach
 
 // TODO(shepshapard): Document.
 
+@OptIn(ExperimentalComposeUiApi::class)
 internal fun down(
     id: Long,
     durationMillis: Long = 0L,
@@ -33,12 +35,14 @@
         durationMillis,
         Offset(x, y),
         true,
+        pressure = 1f,
         durationMillis,
         Offset(x, y),
         false,
         isInitiallyConsumed = false
     )
 
+@OptIn(ExperimentalComposeUiApi::class)
 internal fun PointerInputChange.moveTo(durationMillis: Long, x: Float = 0f, y: Float = 0f) =
     PointerInputChange(
         id = this.id,
@@ -47,10 +51,12 @@
         previousPosition = position,
         uptimeMillis = durationMillis,
         pressed = true,
+        pressure = 1f,
         position = Offset(x, y),
         isInitiallyConsumed = false
     )
 
+@OptIn(ExperimentalComposeUiApi::class)
 internal fun PointerInputChange.moveBy(durationMillis: Long, dx: Float = 0f, dy: Float = 0f) =
     PointerInputChange(
         id = this.id,
@@ -59,10 +65,12 @@
         previousPosition = position,
         uptimeMillis = uptimeMillis + durationMillis,
         pressed = true,
+        pressure = 1f,
         position = Offset(position.x + dx, position.y + dy),
         isInitiallyConsumed = false
     )
 
+@OptIn(ExperimentalComposeUiApi::class)
 internal fun PointerInputChange.up(durationMillis: Long) =
     PointerInputChange(
         id = this.id,
@@ -71,6 +79,7 @@
         previousPosition = position,
         uptimeMillis = durationMillis,
         pressed = false,
+        pressure = 1f,
         position = position,
         isInitiallyConsumed = false
     )
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
index 0611b22..0fd4a55 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
@@ -19,6 +19,7 @@
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collection.mutableVectorOf
 import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.fastMapNotNull
@@ -472,6 +473,7 @@
         }
     }
 
+    @OptIn(ExperimentalComposeUiApi::class)
     override fun onCancel() {
         // Synthesize a cancel event for whatever state we previously saw, if one is applicable.
         // A cancel event is one where all previously down pointers are now up, the change in
@@ -487,6 +489,7 @@
                 position = old.position,
                 uptimeMillis = old.uptimeMillis,
                 pressed = false,
+                pressure = old.pressure,
                 previousPosition = old.position,
                 previousUptimeMillis = old.uptimeMillis,
                 previousPressed = old.pressed,
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ComposeScene.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ComposeScene.desktop.kt
index e83fe3b..9fb410a 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ComposeScene.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/ComposeScene.desktop.kt
@@ -68,6 +68,7 @@
                 position,
                 position,
                 isMousePressed,
+                pressure = 1.0f,
                 type,
                 scrollDelta = scrollDelta
             )
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/input/pointer/TestPointerInputEventData.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/input/pointer/TestPointerInputEventData.skiko.kt
index c99348e..b30311d 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/input/pointer/TestPointerInputEventData.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/input/pointer/TestPointerInputEventData.skiko.kt
@@ -30,5 +30,13 @@
     val down: Boolean
 ) {
     internal fun toPointerInputEventData() =
-        PointerInputEventData(id, uptime, position, position, down, PointerType.Mouse)
+        PointerInputEventData(
+            id,
+            uptime,
+            position,
+            position,
+            down,
+            pressure = 1.0f,
+            PointerType.Mouse
+        )
 }
diff --git a/core/core-ktx/api/current.txt b/core/core-ktx/api/current.txt
index 521f26c..6434f94 100644
--- a/core/core-ktx/api/current.txt
+++ b/core/core-ktx/api/current.txt
@@ -563,6 +563,7 @@
     method public static inline boolean isNotEmpty(android.view.Menu);
     method public static operator java.util.Iterator iterator(android.view.Menu);
     method public static inline operator void minusAssign(android.view.Menu, android.view.MenuItem item);
+    method public static inline void removeItemAt(android.view.Menu, int index);
   }
 
   public final class ViewGroupKt {
diff --git a/core/core-ktx/api/public_plus_experimental_current.txt b/core/core-ktx/api/public_plus_experimental_current.txt
index 521f26c..6434f94 100644
--- a/core/core-ktx/api/public_plus_experimental_current.txt
+++ b/core/core-ktx/api/public_plus_experimental_current.txt
@@ -563,6 +563,7 @@
     method public static inline boolean isNotEmpty(android.view.Menu);
     method public static operator java.util.Iterator iterator(android.view.Menu);
     method public static inline operator void minusAssign(android.view.Menu, android.view.MenuItem item);
+    method public static inline void removeItemAt(android.view.Menu, int index);
   }
 
   public final class ViewGroupKt {
diff --git a/core/core-ktx/api/restricted_current.txt b/core/core-ktx/api/restricted_current.txt
index 521f26c..6434f94 100644
--- a/core/core-ktx/api/restricted_current.txt
+++ b/core/core-ktx/api/restricted_current.txt
@@ -563,6 +563,7 @@
     method public static inline boolean isNotEmpty(android.view.Menu);
     method public static operator java.util.Iterator iterator(android.view.Menu);
     method public static inline operator void minusAssign(android.view.Menu, android.view.MenuItem item);
+    method public static inline void removeItemAt(android.view.Menu, int index);
   }
 
   public final class ViewGroupKt {
diff --git a/core/core-ktx/src/androidTest/java/androidx/core/view/MenuTest.kt b/core/core-ktx/src/androidTest/java/androidx/core/view/MenuTest.kt
index 6e195ce..9d5482a 100644
--- a/core/core-ktx/src/androidTest/java/androidx/core/view/MenuTest.kt
+++ b/core/core-ktx/src/androidTest/java/androidx/core/view/MenuTest.kt
@@ -136,8 +136,8 @@
     }
 
     @Test fun iteratorRemoving() {
-        val item1 = menu.add("")
-        val item2 = menu.add("")
+        val item1 = menu.add(NONE, 9, NONE, "")
+        val item2 = menu.add(NONE, 13, NONE, "")
 
         val iterator = menu.iterator()
 
@@ -163,4 +163,17 @@
             assertSame(items[index], child)
         }
     }
+
+    @Test fun removeItemAt() {
+        val item1 = menu.add(NONE, 9, NONE, "")
+        val item2 = menu.add(NONE, 13, NONE, "")
+
+        menu.removeItemAt(0)
+        assertFalse(item1 in menu)
+        assertEquals(1, menu.size())
+
+        menu.removeItemAt(0)
+        assertFalse(item2 in menu)
+        assertEquals(0, menu.size())
+    }
 }
diff --git a/core/core-ktx/src/main/java/androidx/core/view/Menu.kt b/core/core-ktx/src/main/java/androidx/core/view/Menu.kt
index 804c64b..91095ce 100644
--- a/core/core-ktx/src/main/java/androidx/core/view/Menu.kt
+++ b/core/core-ktx/src/main/java/androidx/core/view/Menu.kt
@@ -71,9 +71,17 @@
         private var index = 0
         override fun hasNext() = index < size()
         override fun next() = getItem(index++) ?: throw IndexOutOfBoundsException()
-        override fun remove() = removeItem(--index)
+        override fun remove() = removeItemAt(--index)
     }
 
+/**
+ * Removes the menu item at the specified index.
+ *
+ * @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the count.
+ */
+public inline fun Menu.removeItemAt(index: Int) =
+    getItem(index)?.let { removeItem(it.itemId) } ?: throw IndexOutOfBoundsException()
+
 /** Returns a [Sequence] over the items in this menu. */
 public val Menu.children: Sequence
     get() = object : Sequence {
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index ab4f0cf..c80fec8 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -327,6 +327,7 @@
     field public static final String EXTRA_PEOPLE_LIST = "android.people.list";
     field public static final String EXTRA_PICTURE = "android.picture";
     field public static final String EXTRA_PICTURE_CONTENT_DESCRIPTION = "android.pictureContentDescription";
+    field public static final String EXTRA_PICTURE_ICON = "android.pictureIcon";
     field public static final String EXTRA_PROGRESS = "android.progress";
     field public static final String EXTRA_PROGRESS_INDETERMINATE = "android.progressIndeterminate";
     field public static final String EXTRA_PROGRESS_MAX = "android.progressMax";
@@ -451,6 +452,7 @@
     ctor public NotificationCompat.BigPictureStyle(androidx.core.app.NotificationCompat.Builder?);
     method public androidx.core.app.NotificationCompat.BigPictureStyle bigLargeIcon(android.graphics.Bitmap?);
     method public androidx.core.app.NotificationCompat.BigPictureStyle bigPicture(android.graphics.Bitmap?);
+    method @RequiresApi(31) public androidx.core.app.NotificationCompat.BigPictureStyle bigPicture(android.graphics.drawable.Icon?);
     method public androidx.core.app.NotificationCompat.BigPictureStyle setBigContentTitle(CharSequence?);
     method @RequiresApi(31) public androidx.core.app.NotificationCompat.BigPictureStyle setContentDescription(CharSequence?);
     method public androidx.core.app.NotificationCompat.BigPictureStyle setSummaryText(CharSequence?);
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index aa1b219..4ffe2f01 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -327,6 +327,7 @@
     field public static final String EXTRA_PEOPLE_LIST = "android.people.list";
     field public static final String EXTRA_PICTURE = "android.picture";
     field public static final String EXTRA_PICTURE_CONTENT_DESCRIPTION = "android.pictureContentDescription";
+    field public static final String EXTRA_PICTURE_ICON = "android.pictureIcon";
     field public static final String EXTRA_PROGRESS = "android.progress";
     field public static final String EXTRA_PROGRESS_INDETERMINATE = "android.progressIndeterminate";
     field public static final String EXTRA_PROGRESS_MAX = "android.progressMax";
@@ -451,6 +452,7 @@
     ctor public NotificationCompat.BigPictureStyle(androidx.core.app.NotificationCompat.Builder?);
     method public androidx.core.app.NotificationCompat.BigPictureStyle bigLargeIcon(android.graphics.Bitmap?);
     method public androidx.core.app.NotificationCompat.BigPictureStyle bigPicture(android.graphics.Bitmap?);
+    method @RequiresApi(31) public androidx.core.app.NotificationCompat.BigPictureStyle bigPicture(android.graphics.drawable.Icon?);
     method public androidx.core.app.NotificationCompat.BigPictureStyle setBigContentTitle(CharSequence?);
     method @RequiresApi(31) public androidx.core.app.NotificationCompat.BigPictureStyle setContentDescription(CharSequence?);
     method public androidx.core.app.NotificationCompat.BigPictureStyle setSummaryText(CharSequence?);
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index b9ff456..385b784 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -373,6 +373,7 @@
     field public static final String EXTRA_PEOPLE_LIST = "android.people.list";
     field public static final String EXTRA_PICTURE = "android.picture";
     field public static final String EXTRA_PICTURE_CONTENT_DESCRIPTION = "android.pictureContentDescription";
+    field public static final String EXTRA_PICTURE_ICON = "android.pictureIcon";
     field public static final String EXTRA_PROGRESS = "android.progress";
     field public static final String EXTRA_PROGRESS_INDETERMINATE = "android.progressIndeterminate";
     field public static final String EXTRA_PROGRESS_MAX = "android.progressMax";
@@ -501,6 +502,8 @@
     ctor public NotificationCompat.BigPictureStyle(androidx.core.app.NotificationCompat.Builder?);
     method public androidx.core.app.NotificationCompat.BigPictureStyle bigLargeIcon(android.graphics.Bitmap?);
     method public androidx.core.app.NotificationCompat.BigPictureStyle bigPicture(android.graphics.Bitmap?);
+    method @RequiresApi(31) public androidx.core.app.NotificationCompat.BigPictureStyle bigPicture(android.graphics.drawable.Icon?);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static androidx.core.graphics.drawable.IconCompat? getPictureIcon(android.os.Bundle?);
     method public androidx.core.app.NotificationCompat.BigPictureStyle setBigContentTitle(CharSequence?);
     method @RequiresApi(31) public androidx.core.app.NotificationCompat.BigPictureStyle setContentDescription(CharSequence?);
     method public androidx.core.app.NotificationCompat.BigPictureStyle setSummaryText(CharSequence?);
diff --git a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
index fe25eab..ea6b724 100644
--- a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
@@ -1509,6 +1509,127 @@
         assertEquals(Icon.TYPE_RESOURCE, rebuiltIcon.getType());
     }
 
+    @SdkSuppress(minSdkVersion = 19, maxSdkVersion = 30)
+    @Test
+    // minSdkVersion selected because extras are not populated before 19.
+    // maxSdkVersion selected because EXTRA_PICTURE_ICON not set before 31.
+    public void testBigPictureStyle_bigPictureWithBitmap_legacy() {
+
+        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),
+                R.drawable.notification_bg_low_pressed);
+        Notification n = new NotificationCompat.Builder(mContext, "channelId")
+                .setSmallIcon(1)
+                .setStyle(new NotificationCompat.BigPictureStyle()
+                        .bigPicture(bitmap))
+                .build();
+
+        Bundle extras = NotificationCompat.getExtras(n);
+        assertNotNull(extras);
+        // Because the bigPicture was a bitmap, it'll be stored in EXTRA_PICTURE.
+        assertTrue(extras.containsKey(NotificationCompat.EXTRA_PICTURE));
+        assertNotNull(extras.get(NotificationCompat.EXTRA_PICTURE));
+        // EXTRA_PICTURE_ICON was not set before 31.
+        assertFalse(extras.containsKey(NotificationCompat.EXTRA_PICTURE_ICON));
+
+        Parcelable firstBuiltIcon = n.extras.getParcelable(NotificationCompat.EXTRA_PICTURE);
+        // Checks that the data is unparcled as a bitmap. Because the original data was a
+        // bitmap, this ensures maximal backwards compatibility for existing listeners expecting
+        // a bitmap from extras.
+        assertSame(Bitmap.class, firstBuiltIcon.getClass());
+
+        // Ensures that the icon's EXTRA_PICTURE gets rebuilt from style.
+        Style style = Style.extractStyleFromNotification(n);
+        assertNotNull(style);
+        assertSame(NotificationCompat.BigPictureStyle.class, style.getClass());
+        n = new NotificationCompat.Builder(mContext, "channelId")
+                .setSmallIcon(1)
+                .setStyle(style)
+                .build();
+        Parcelable rebuiltIcon = n.extras.getParcelable(NotificationCompat.EXTRA_PICTURE);
+        assertSame(Bitmap.class, rebuiltIcon.getClass());
+
+    }
+
+    @SdkSuppress(minSdkVersion = 31)
+    @Test
+    // minSdkVersion selected because EXTRA_PICTURE_ICON not set before 31.
+    public void testBigPictureStyle_bigPictureWithBitmap() {
+
+        Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),
+                R.drawable.notification_bg_low_pressed);
+        Notification n = new NotificationCompat.Builder(mContext, "channelId")
+                .setSmallIcon(1)
+                .setStyle(new NotificationCompat.BigPictureStyle()
+                        .bigPicture(bitmap))
+                .build();
+
+        Bundle extras = NotificationCompat.getExtras(n);
+        assertNotNull(extras);
+        // Because the bigPicture was a bitmap, it'll be stored in EXTRA_PICTURE.
+        assertTrue(extras.containsKey(NotificationCompat.EXTRA_PICTURE));
+        assertNotNull(extras.get(NotificationCompat.EXTRA_PICTURE));
+        // EXTRA_PICTURE_ICON began being set in 31.
+        // However, we avoid storing both the Icon version and the Bitmap
+        // version in extras, so the stored value is null.
+        assertTrue(extras.containsKey(NotificationCompat.EXTRA_PICTURE_ICON));
+        assertNull(extras.get(NotificationCompat.EXTRA_PICTURE_ICON));
+
+        Parcelable firstBuiltIcon = n.extras.getParcelable(NotificationCompat.EXTRA_PICTURE);
+        // Checks that the data is unparcled as a bitmap. Because the original data was a
+        // bitmap, this ensures maximal backwards compatibility for existing listeners expecting
+        // a bitmap from extras.
+        assertSame(Bitmap.class, firstBuiltIcon.getClass());
+
+        // Ensures that the icon's EXTRA_PICTURE gets rebuilt from style.
+        Style style = Style.extractStyleFromNotification(n);
+        assertNotNull(style);
+        assertSame(NotificationCompat.BigPictureStyle.class, style.getClass());
+        n = new NotificationCompat.Builder(mContext, "channelId")
+                .setSmallIcon(1)
+                .setStyle(style)
+                .build();
+        Parcelable rebuiltIcon = n.extras.getParcelable(NotificationCompat.EXTRA_PICTURE);
+        assertSame(Bitmap.class, rebuiltIcon.getClass());
+
+    }
+
+    @SdkSuppress(minSdkVersion = 31)
+    @Test
+    // minSdkVersion selected because setting BigPicture from Icon added in 31.
+    public void testBigPictureStyle_bigPictureWithIcon() {
+        Notification n = new NotificationCompat.Builder(mContext)
+                .setSmallIcon(1)
+                .setStyle(new NotificationCompat.BigPictureStyle()
+                        .bigPicture(Icon.createWithResource(mContext,
+                                R.drawable.notification_template_icon_bg)))
+                .build();
+
+        Bundle extras = NotificationCompat.getExtras(n);
+        assertNotNull(extras);
+        // Because the notification was created with a non-bitmap bigPicture, it will be
+        // stored in EXTRA_PICTURE_ICON.
+        assertTrue(extras.containsKey(NotificationCompat.EXTRA_PICTURE_ICON));
+        assertNotNull(extras.get(NotificationCompat.EXTRA_PICTURE_ICON));
+        // We avoid storing both the Icon version and the Bitmap version in extras.
+        assertTrue(extras.containsKey(NotificationCompat.EXTRA_PICTURE));
+        assertNull(extras.get(NotificationCompat.EXTRA_PICTURE));
+
+        Icon firstBuiltIcon = n.extras.getParcelable(NotificationCompat.EXTRA_PICTURE_ICON);
+        assertEquals(Icon.TYPE_RESOURCE, firstBuiltIcon.getType());
+
+        // Ensures that the icon's EXTRA_PICTURE gets rebuilt.
+        Style style = Style.extractStyleFromNotification(n);
+        assertNotNull(style);
+        assertSame(NotificationCompat.BigPictureStyle.class, style.getClass());
+        n = new NotificationCompat.Builder(mContext, "channelId")
+                .setSmallIcon(1)
+                .setStyle(style)
+                .build();
+
+        Icon rebuiltIcon = n.extras.getParcelable(NotificationCompat.EXTRA_PICTURE_ICON);
+        assertEquals(Icon.TYPE_RESOURCE, rebuiltIcon.getType());
+    }
+
     @SdkSuppress(minSdkVersion = 16)
     @Test
     public void testMessagingStyle_nullPerson() {
diff --git a/core/core/src/androidTest/java/androidx/core/view/AccessibilityDelegateCompatTest.java b/core/core/src/androidTest/java/androidx/core/view/AccessibilityDelegateCompatTest.java
index 350d71c..64623b1 100644
--- a/core/core/src/androidTest/java/androidx/core/view/AccessibilityDelegateCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/view/AccessibilityDelegateCompatTest.java
@@ -589,7 +589,7 @@
 
     @FlakyTest(bugId = 206644987)
     @Test
-    @SdkSuppress(minSdkVersion = 19)
+    @SdkSuppress(minSdkVersion = 19, maxSdkVersion = 32) // API 33 fails 100% b/233396250
     public void testSetAccessibilityPaneTitle_sendsOutCorrectEvent() throws TimeoutException {
         final Activity activity = mActivityTestRule.getActivity();
         final CharSequence title = "Sample title";
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompat.java b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
index 196746b..3ba98ca 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -436,6 +436,14 @@
     public static final String EXTRA_PICTURE = "android.picture";
 
     /**
+     * {@link #getExtras extras} key: this is an {@link Icon} of an image to be
+     * shown in {@link Notification.BigPictureStyle} expanded notifications, supplied to
+     * {@link BigPictureStyle#bigPicture(Icon)}.
+     */
+    @SuppressLint("ActionValue") // Field & value copied from android.app.Notification
+    public static final String EXTRA_PICTURE_ICON = "android.pictureIcon";
+
+    /**
      * {@link #extras} key: this is a content description of the big picture supplied from
      * {@link BigPictureStyle#bigPicture(Bitmap)}, supplied to
      * {@link BigPictureStyle#setContentDescription(CharSequence)}.
@@ -2740,7 +2748,8 @@
             if (extras.containsKey(EXTRA_SELF_DISPLAY_NAME)
                     || extras.containsKey(EXTRA_MESSAGING_STYLE_USER)) {
                 return new MessagingStyle();
-            } else if (extras.containsKey(EXTRA_PICTURE)) {
+            } else if (extras.containsKey(EXTRA_PICTURE)
+                    || extras.containsKey(EXTRA_PICTURE_ICON)) {
                 return new BigPictureStyle();
             } else if (extras.containsKey(EXTRA_BIG_TEXT)) {
                 return new BigTextStyle();
@@ -3022,7 +3031,7 @@
      * 
* This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like so: *

-     * Notification notification = new Notification.Builder(mContext)
+     * Notification notification = new NotificationCompat.Builder(mContext)
      *     .setContentTitle("New photo from " + sender.toString())
      *     .setContentText(subject)
      *     .setSmallIcon(R.drawable.new_post)
@@ -3039,7 +3048,7 @@
         private static final String TEMPLATE_CLASS_NAME =
                 "androidx.core.app.NotificationCompat$BigPictureStyle";
 
-        private Bitmap mPicture;
+        private IconCompat mPictureIcon;
         private IconCompat mBigLargeIcon;
         private boolean mBigLargeIconSet;
         private CharSequence mPictureContentDescription;
@@ -3085,7 +3094,17 @@
          * Provide the bitmap to be used as the payload for the BigPicture notification.
          */
         public @NonNull BigPictureStyle bigPicture(@Nullable Bitmap b) {
-            mPicture = b;
+            mPictureIcon = b == null ? null : IconCompat.createWithBitmap(b);
+            return this;
+        }
+
+        /**
+         * Provide an icon to be used as the payload for the BigPicture notification.
+         * Note that certain features (like animated Icons) may not work on all versions.
+         */
+        @RequiresApi(31)
+        public @NonNull BigPictureStyle bigPicture(@Nullable Icon i) {
+            mPictureIcon = IconCompat.createFromIcon(i);
             return this;
         }
 
@@ -3129,8 +3148,21 @@
             if (Build.VERSION.SDK_INT >= 16) {
                 Notification.BigPictureStyle style =
                         new Notification.BigPictureStyle(builder.getBuilder())
-                                .setBigContentTitle(mBigContentTitle)
-                                .bigPicture(mPicture);
+                                .setBigContentTitle(mBigContentTitle);
+                if (mPictureIcon != null) {
+                    // Attempts to set the icon for BigPictureStyle; prefers using data as Icon,
+                    // with a fallback to store the Bitmap if Icon is not supported directly.
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                        Context context = null;
+                        if (builder instanceof NotificationCompatBuilder) {
+                            context = ((NotificationCompatBuilder) builder).getContext();
+                        }
+                        Api31Impl.setBigPicture(style, mPictureIcon.toIcon(context));
+                    } else if (mPictureIcon.getType() == IconCompat.TYPE_BITMAP) {
+                        style = style.bigPicture(mPictureIcon.getBitmap());
+                    }
+                }
+                // Attempts to set the big large icon for BigPictureStyle.
                 if (mBigLargeIconSet) {
                     if (mBigLargeIcon == null) {
                         Api16Impl.setBigLargeIcon(style, null);
@@ -3172,10 +3204,28 @@
                 mBigLargeIcon = asIconCompat(extras.getParcelable(EXTRA_LARGE_ICON_BIG));
                 mBigLargeIconSet = true;
             }
-            mPicture = extras.getParcelable(EXTRA_PICTURE);
+            mPictureIcon = getPictureIcon(extras);
             mShowBigPictureWhenCollapsed = extras.getBoolean(EXTRA_SHOW_BIG_PICTURE_WHEN_COLLAPSED);
         }
 
+        /**
+         * @hide
+         */
+        @RestrictTo(LIBRARY_GROUP_PREFIX)
+        @Nullable
+        public static IconCompat getPictureIcon(@Nullable Bundle extras) {
+            if (extras == null) return null;
+            // When this style adds a picture, we only add one of the keys.  If both were added,
+            // it would most likely be a legacy app trying to override the picture in some way.
+            // Because of that case it's better to give precedence to the legacy field.
+            Parcelable bitmapPicture = extras.getParcelable(EXTRA_PICTURE);
+            if (bitmapPicture != null) {
+                return asIconCompat(bitmapPicture);
+            } else {
+                return asIconCompat(extras.getParcelable(EXTRA_PICTURE_ICON));
+            }
+        }
+
         @Nullable
         private static IconCompat asIconCompat(@Nullable Parcelable bitmapOrIcon) {
             if (bitmapOrIcon != null) {
@@ -3200,6 +3250,7 @@
             super.clearCompatExtraKeys(extras);
             extras.remove(EXTRA_LARGE_ICON_BIG);
             extras.remove(EXTRA_PICTURE);
+            extras.remove(EXTRA_PICTURE_ICON);
             extras.remove(EXTRA_SHOW_BIG_PICTURE_WHEN_COLLAPSED);
         }
 
@@ -3276,6 +3327,14 @@
                     CharSequence contentDescription) {
                 style.setContentDescription(contentDescription);
             }
+
+            /**
+             * Calls {@link Notification.BigPictureStyle#bigPicture(Icon)}
+             */
+            @RequiresApi(31)
+            static void setBigPicture(Notification.BigPictureStyle style, Icon icon) {
+                style.bigPicture(icon);
+            }
         }
     }
 
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 45e2858..f3eac5e 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -238,6 +238,7 @@
 Found an unresolved type in androidx\.slice\.builders\$list\(android\.content\.Context, android\.net\.Uri, kotlin\.Long, kotlin\.Function[0-9]+\(\(androidx\.slice\.builders\.ListBuilderDsl, kotlin\.Unit\)\)\) \(ListBuilder\.kt:[0-9]+\)
 # See b/180023439 for hiltNavGraphViewModel warning.
 Unresolved link to .*
+Found an unresolved type in androidx\.core\.view\$removeItemAt\(android\.view\.Menu, kotlin\.Int\) \(Menu\.kt:[0-9]+\)
 Found an unresolved type in androidx\.compose\.foundation\.MutatorMutex\$mutate\(androidx\.compose\.foundation\.MutatePriority, kotlin\.coroutines\.SuspendFunction[0-9]+\(\(androidx\.compose\.foundation\.MutatorMutex\.mutate\.R\)\)\) \(MutatorMutex\.kt:[0-9]+\)
 Found an unresolved type in androidx\.compose\.foundation\.MutatorMutex\$mutateWith\(androidx\.compose\.foundation\.MutatorMutex\.mutateWith\.T, androidx\.compose\.foundation\.MutatePriority, kotlin\.coroutines\.SuspendFunction[0-9]+\(\(androidx\.compose\.foundation\.MutatorMutex\.mutateWith\.T, androidx\.compose\.foundation\.MutatorMutex\.mutateWith\.R\)\)\) \(MutatorMutex\.kt:[0-9]+\)
 Found an unresolved type in androidx\.compose\.foundation\.gestures\$detectTapGestures\(androidx\.compose\.ui\.input\.pointer\.PointerInputScope, kotlin\.Function[0-9]+\(\(androidx\.compose\.ui\.geometry\.Offset, kotlin\.Unit\)\), kotlin\.Function[0-9]+\(\(androidx\.compose\.ui\.geometry\.Offset, kotlin\.Unit\)\), kotlin\.coroutines\.SuspendFunction[0-9]+\(\(androidx\.compose\.foundation\.gestures\.PressGestureScope, androidx\.compose\.ui\.geometry\.Offset, kotlin\.Unit\)\), kotlin\.Function[0-9]+\(\(androidx\.compose\.ui\.geometry\.Offset, kotlin\.Unit\)\)\) \(TapGestureDetector\.kt:[0-9]+\)
@@ -432,8 +433,7 @@
 # Linux builds cannot build mac, hence we get this warning.
 # see: https://github.com/JetBrains/kotlin/blob/master/native/commonizer/README.md
 # This warning is printed from: https://github.com/JetBrains/kotlin/blob/bc853e45e8982eff74e3263b0197c1af6086615d/native/commonizer/src/org/jetbrains/kotlin/commonizer/konan/LibraryCommonizer.kt#L41
-Warning\: No libraries found for target macos_arm[0-9]+\. This target will be excluded from commonization\.
-Warning\: No libraries found for target macos_x[0-9]+\. This target will be excluded from commonization\.
+Warning\: No libraries found for target (macos|ios|ios_simulator)_(arm|x)[0-9]+\. This target will be excluded from commonization\.
 # > Task :tv:tv-foundation:processDebugManifest
 # > Task :tv:tv-material:processDebugManifest
 # > Task :tv:tv-material:processReleaseManifest
diff --git a/development/referenceDocs/stageReferenceDocsWithDackka.sh b/development/referenceDocs/stageReferenceDocsWithDackka.sh
index 0a87b08..2decd5d 100755
--- a/development/referenceDocs/stageReferenceDocsWithDackka.sh
+++ b/development/referenceDocs/stageReferenceDocsWithDackka.sh
@@ -51,10 +51,12 @@
   "activity"
   "annotation"
   "arch"
+  "asynclayoutinflater"
   "autofill"
 #  "benchmark"
   "cardview"
   "collection"
+  "coordinatorlayout"
   "core"
   "drawerlayout"
   "emoji"
@@ -67,6 +69,7 @@
   "navigation"
   "paging"
   "savedstate"
+  "slidingpanelayout"
   "vectordrawable"
   "viewpager"
   "viewpager2"
@@ -77,11 +80,13 @@
   "activity"
   "annotation"
   "arch"
+  "asynclayoutinflater"
   "autofill"
 #  "benchmark"
   "cardview"
   "compose"
   "collection"
+  "coordinatorlayout"
   "core"
   "drawerlayout"
   "emoji"
@@ -94,6 +99,7 @@
   "navigation"
   "paging"
   "savedstate"
+  "slidingpanelayout"
   "vectordrawable"
   "viewpager"
   "viewpager2"
diff --git a/development/studio/studio.vmoptions b/development/studio/studio.vmoptions
index ae65e63..e78bec1 100644
--- a/development/studio/studio.vmoptions
+++ b/development/studio/studio.vmoptions
@@ -1 +1,2 @@
 -Xmx8g
+-Djava.net.preferIPv6Addresses=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 630cc0f..a707641 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -30,11 +30,11 @@
 guavaJre = "31.1-jre"
 hilt = "2.42"
 incap = "0.2"
-kotlin = "1.7.0"
-kotlinNative = "1.7.0"
+kotlin = "1.7.10"
+kotlinNative = "1.7.10"
 kotlinCompileTesting = "1.4.1"
 kotlinCoroutines = "1.6.1"
-ksp = "1.7.0-1.0.6"
+ksp = "1.7.10-1.0.6"
 ktlint = "0.46.0-20220520.192227-74"
 leakcanary = "2.8.1"
 metalava = "1.0.0-alpha06"
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 0cbbb70..ea6f9642 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -1238,21 +1238,21 @@
          
       
       
-      
-         
-            
+      
+         
+            
          
       
 
-      
-         
-            
+      
+         
+            
          
       
 
-      
-         
-            
+      
+         
+            
          
       
    
diff --git a/health/health-connect-client/api/current.txt b/health/health-connect-client/api/current.txt
index 57d6ee29..bef80bc 100644
--- a/health/health-connect-client/api/current.txt
+++ b/health/health-connect-client/api/current.txt
@@ -45,10 +45,10 @@
   public final class AggregationResult {
     method public operator boolean contains(androidx.health.connect.client.aggregate.AggregateMetric metric);
     method public operator  T? get(androidx.health.connect.client.aggregate.AggregateMetric metric);
-    method public java.util.List getDataOrigins();
+    method public java.util.Set getDataOrigins();
     method @Deprecated public  T? getMetric(androidx.health.connect.client.aggregate.AggregateMetric metric);
     method @Deprecated public boolean hasMetric(androidx.health.connect.client.aggregate.AggregateMetric metric);
-    property public final java.util.List dataOrigins;
+    property public final java.util.Set dataOrigins;
   }
 
   public final class AggregationResultGroupedByDuration {
@@ -162,7 +162,7 @@
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric BASAL_CALORIES_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric BASAL_CALORIES_TOTAL;
     field public static final androidx.health.connect.client.records.BasalMetabolicRateRecord.Companion Companion;
   }
 
@@ -323,26 +323,18 @@
     field public static final String MEDIUM = "medium";
   }
 
-  public final class CyclingPedalingCadence {
-    ctor public CyclingPedalingCadence(java.time.Instant time, @FloatRange(from=0.0, to=10000.0) double revolutionsPerMinute);
-    method public double getRevolutionsPerMinute();
-    method public java.time.Instant getTime();
-    property public final double revolutionsPerMinute;
-    property public final java.time.Instant time;
-  }
-
   public final class CyclingPedalingCadenceRecord implements androidx.health.connect.client.records.Record {
-    ctor public CyclingPedalingCadenceRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public CyclingPedalingCadenceRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.util.List getSamples();
+    method public java.util.List getSamples();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.util.List samples;
+    property public java.util.List samples;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
     field public static final androidx.health.connect.client.records.CyclingPedalingCadenceRecord.Companion Companion;
@@ -354,6 +346,14 @@
   public static final class CyclingPedalingCadenceRecord.Companion {
   }
 
+  public static final class CyclingPedalingCadenceRecord.Sample {
+    ctor public CyclingPedalingCadenceRecord.Sample(java.time.Instant time, @FloatRange(from=0.0, to=10000.0) double revolutionsPerMinute);
+    method public double getRevolutionsPerMinute();
+    method public java.time.Instant getTime();
+    property public final double revolutionsPerMinute;
+    property public final java.time.Instant time;
+  }
+
   public final class DistanceRecord implements androidx.health.connect.client.records.Record {
     ctor public DistanceRecord(androidx.health.connect.client.units.Length distance, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.units.Length getDistance();
@@ -620,26 +620,18 @@
   public static final class FloorsClimbedRecord.Companion {
   }
 
-  public final class HeartRate {
-    ctor public HeartRate(java.time.Instant time, @IntRange(from=1L, to=300L) long beatsPerMinute);
-    method public long getBeatsPerMinute();
-    method public java.time.Instant getTime();
-    property public final long beatsPerMinute;
-    property public final java.time.Instant time;
-  }
-
   public final class HeartRateRecord implements androidx.health.connect.client.records.Record {
-    ctor public HeartRateRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public HeartRateRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.util.List getSamples();
+    method public java.util.List getSamples();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.util.List samples;
+    property public java.util.List samples;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
     field public static final androidx.health.connect.client.aggregate.AggregateMetric BPM_AVG;
@@ -652,6 +644,14 @@
   public static final class HeartRateRecord.Companion {
   }
 
+  public static final class HeartRateRecord.Sample {
+    ctor public HeartRateRecord.Sample(java.time.Instant time, @IntRange(from=1L, to=300L) long beatsPerMinute);
+    method public long getBeatsPerMinute();
+    method public java.time.Instant getTime();
+    property public final long beatsPerMinute;
+    property public final java.time.Instant time;
+  }
+
   public final class HeightRecord implements androidx.health.connect.client.records.Record {
     ctor public HeightRecord(androidx.health.connect.client.units.Length height, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.units.Length getHeight();
@@ -1099,26 +1099,18 @@
     property public final java.time.Instant time;
   }
 
-  public final class StepsCadence {
-    ctor public StepsCadence(java.time.Instant time, @FloatRange(from=0.0, to=10000.0) double rate);
-    method public double getRate();
-    method public java.time.Instant getTime();
-    property public final double rate;
-    property public final java.time.Instant time;
-  }
-
   public final class StepsCadenceRecord implements androidx.health.connect.client.records.Record {
-    ctor public StepsCadenceRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public StepsCadenceRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.util.List getSamples();
+    method public java.util.List getSamples();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.util.List samples;
+    property public java.util.List samples;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
     field public static final androidx.health.connect.client.records.StepsCadenceRecord.Companion Companion;
@@ -1130,6 +1122,14 @@
   public static final class StepsCadenceRecord.Companion {
   }
 
+  public static final class StepsCadenceRecord.Sample {
+    ctor public StepsCadenceRecord.Sample(java.time.Instant time, @FloatRange(from=0.0, to=10000.0) double rate);
+    method public double getRate();
+    method public java.time.Instant getTime();
+    property public final double rate;
+    property public final java.time.Instant time;
+  }
+
   public final class StepsRecord implements androidx.health.connect.client.records.Record {
     ctor public StepsRecord(long count, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public long getCount();
@@ -1330,15 +1330,15 @@
 package androidx.health.connect.client.request {
 
   public final class AggregateGroupByDurationRequest {
-    ctor public AggregateGroupByDurationRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, java.time.Duration timeRangeSlicer, optional java.util.List dataOriginFilter);
+    ctor public AggregateGroupByDurationRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, java.time.Duration timeRangeSlicer, optional java.util.Set dataOriginFilter);
   }
 
   public final class AggregateGroupByPeriodRequest {
-    ctor public AggregateGroupByPeriodRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, java.time.Period timeRangeSlicer, optional java.util.List dataOriginFilter);
+    ctor public AggregateGroupByPeriodRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, java.time.Period timeRangeSlicer, optional java.util.Set dataOriginFilter);
   }
 
   public final class AggregateRequest {
-    ctor public AggregateRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, optional java.util.List dataOriginFilter);
+    ctor public AggregateRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, optional java.util.Set dataOriginFilter);
   }
 
   public final class ChangesTokenRequest {
@@ -1346,7 +1346,7 @@
   }
 
   public final class ReadRecordsRequest {
-    ctor public ReadRecordsRequest(kotlin.reflect.KClass recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, optional java.util.List dataOriginFilter, optional boolean ascendingOrder, optional int pageSize, optional String? pageToken);
+    ctor public ReadRecordsRequest(kotlin.reflect.KClass recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, optional java.util.Set dataOriginFilter, optional boolean ascendingOrder, optional int pageSize, optional String? pageToken);
   }
 
 }
diff --git a/health/health-connect-client/api/public_plus_experimental_current.txt b/health/health-connect-client/api/public_plus_experimental_current.txt
index 57d6ee29..bef80bc 100644
--- a/health/health-connect-client/api/public_plus_experimental_current.txt
+++ b/health/health-connect-client/api/public_plus_experimental_current.txt
@@ -45,10 +45,10 @@
   public final class AggregationResult {
     method public operator boolean contains(androidx.health.connect.client.aggregate.AggregateMetric metric);
     method public operator  T? get(androidx.health.connect.client.aggregate.AggregateMetric metric);
-    method public java.util.List getDataOrigins();
+    method public java.util.Set getDataOrigins();
     method @Deprecated public  T? getMetric(androidx.health.connect.client.aggregate.AggregateMetric metric);
     method @Deprecated public boolean hasMetric(androidx.health.connect.client.aggregate.AggregateMetric metric);
-    property public final java.util.List dataOrigins;
+    property public final java.util.Set dataOrigins;
   }
 
   public final class AggregationResultGroupedByDuration {
@@ -162,7 +162,7 @@
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric BASAL_CALORIES_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric BASAL_CALORIES_TOTAL;
     field public static final androidx.health.connect.client.records.BasalMetabolicRateRecord.Companion Companion;
   }
 
@@ -323,26 +323,18 @@
     field public static final String MEDIUM = "medium";
   }
 
-  public final class CyclingPedalingCadence {
-    ctor public CyclingPedalingCadence(java.time.Instant time, @FloatRange(from=0.0, to=10000.0) double revolutionsPerMinute);
-    method public double getRevolutionsPerMinute();
-    method public java.time.Instant getTime();
-    property public final double revolutionsPerMinute;
-    property public final java.time.Instant time;
-  }
-
   public final class CyclingPedalingCadenceRecord implements androidx.health.connect.client.records.Record {
-    ctor public CyclingPedalingCadenceRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public CyclingPedalingCadenceRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.util.List getSamples();
+    method public java.util.List getSamples();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.util.List samples;
+    property public java.util.List samples;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
     field public static final androidx.health.connect.client.records.CyclingPedalingCadenceRecord.Companion Companion;
@@ -354,6 +346,14 @@
   public static final class CyclingPedalingCadenceRecord.Companion {
   }
 
+  public static final class CyclingPedalingCadenceRecord.Sample {
+    ctor public CyclingPedalingCadenceRecord.Sample(java.time.Instant time, @FloatRange(from=0.0, to=10000.0) double revolutionsPerMinute);
+    method public double getRevolutionsPerMinute();
+    method public java.time.Instant getTime();
+    property public final double revolutionsPerMinute;
+    property public final java.time.Instant time;
+  }
+
   public final class DistanceRecord implements androidx.health.connect.client.records.Record {
     ctor public DistanceRecord(androidx.health.connect.client.units.Length distance, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.units.Length getDistance();
@@ -620,26 +620,18 @@
   public static final class FloorsClimbedRecord.Companion {
   }
 
-  public final class HeartRate {
-    ctor public HeartRate(java.time.Instant time, @IntRange(from=1L, to=300L) long beatsPerMinute);
-    method public long getBeatsPerMinute();
-    method public java.time.Instant getTime();
-    property public final long beatsPerMinute;
-    property public final java.time.Instant time;
-  }
-
   public final class HeartRateRecord implements androidx.health.connect.client.records.Record {
-    ctor public HeartRateRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public HeartRateRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.util.List getSamples();
+    method public java.util.List getSamples();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.util.List samples;
+    property public java.util.List samples;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
     field public static final androidx.health.connect.client.aggregate.AggregateMetric BPM_AVG;
@@ -652,6 +644,14 @@
   public static final class HeartRateRecord.Companion {
   }
 
+  public static final class HeartRateRecord.Sample {
+    ctor public HeartRateRecord.Sample(java.time.Instant time, @IntRange(from=1L, to=300L) long beatsPerMinute);
+    method public long getBeatsPerMinute();
+    method public java.time.Instant getTime();
+    property public final long beatsPerMinute;
+    property public final java.time.Instant time;
+  }
+
   public final class HeightRecord implements androidx.health.connect.client.records.Record {
     ctor public HeightRecord(androidx.health.connect.client.units.Length height, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.units.Length getHeight();
@@ -1099,26 +1099,18 @@
     property public final java.time.Instant time;
   }
 
-  public final class StepsCadence {
-    ctor public StepsCadence(java.time.Instant time, @FloatRange(from=0.0, to=10000.0) double rate);
-    method public double getRate();
-    method public java.time.Instant getTime();
-    property public final double rate;
-    property public final java.time.Instant time;
-  }
-
   public final class StepsCadenceRecord implements androidx.health.connect.client.records.Record {
-    ctor public StepsCadenceRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    ctor public StepsCadenceRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.util.List getSamples();
+    method public java.util.List getSamples();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.util.List samples;
+    property public java.util.List samples;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
     field public static final androidx.health.connect.client.records.StepsCadenceRecord.Companion Companion;
@@ -1130,6 +1122,14 @@
   public static final class StepsCadenceRecord.Companion {
   }
 
+  public static final class StepsCadenceRecord.Sample {
+    ctor public StepsCadenceRecord.Sample(java.time.Instant time, @FloatRange(from=0.0, to=10000.0) double rate);
+    method public double getRate();
+    method public java.time.Instant getTime();
+    property public final double rate;
+    property public final java.time.Instant time;
+  }
+
   public final class StepsRecord implements androidx.health.connect.client.records.Record {
     ctor public StepsRecord(long count, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public long getCount();
@@ -1330,15 +1330,15 @@
 package androidx.health.connect.client.request {
 
   public final class AggregateGroupByDurationRequest {
-    ctor public AggregateGroupByDurationRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, java.time.Duration timeRangeSlicer, optional java.util.List dataOriginFilter);
+    ctor public AggregateGroupByDurationRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, java.time.Duration timeRangeSlicer, optional java.util.Set dataOriginFilter);
   }
 
   public final class AggregateGroupByPeriodRequest {
-    ctor public AggregateGroupByPeriodRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, java.time.Period timeRangeSlicer, optional java.util.List dataOriginFilter);
+    ctor public AggregateGroupByPeriodRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, java.time.Period timeRangeSlicer, optional java.util.Set dataOriginFilter);
   }
 
   public final class AggregateRequest {
-    ctor public AggregateRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, optional java.util.List dataOriginFilter);
+    ctor public AggregateRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, optional java.util.Set dataOriginFilter);
   }
 
   public final class ChangesTokenRequest {
@@ -1346,7 +1346,7 @@
   }
 
   public final class ReadRecordsRequest {
-    ctor public ReadRecordsRequest(kotlin.reflect.KClass recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, optional java.util.List dataOriginFilter, optional boolean ascendingOrder, optional int pageSize, optional String? pageToken);
+    ctor public ReadRecordsRequest(kotlin.reflect.KClass recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, optional java.util.Set dataOriginFilter, optional boolean ascendingOrder, optional int pageSize, optional String? pageToken);
   }
 
 }
diff --git a/health/health-connect-client/api/restricted_current.txt b/health/health-connect-client/api/restricted_current.txt
index 27ca15d..c4fcb00 100644
--- a/health/health-connect-client/api/restricted_current.txt
+++ b/health/health-connect-client/api/restricted_current.txt
@@ -45,10 +45,10 @@
   public final class AggregationResult {
     method public operator boolean contains(androidx.health.connect.client.aggregate.AggregateMetric metric);
     method public operator  T? get(androidx.health.connect.client.aggregate.AggregateMetric metric);
-    method public java.util.List getDataOrigins();
+    method public java.util.Set getDataOrigins();
     method @Deprecated public  T? getMetric(androidx.health.connect.client.aggregate.AggregateMetric metric);
     method @Deprecated public boolean hasMetric(androidx.health.connect.client.aggregate.AggregateMetric metric);
-    property public final java.util.List dataOrigins;
+    property public final java.util.Set dataOrigins;
   }
 
   public final class AggregationResultGroupedByDuration {
@@ -162,7 +162,7 @@
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
-    field public static final androidx.health.connect.client.aggregate.AggregateMetric BASAL_CALORIES_TOTAL;
+    field public static final androidx.health.connect.client.aggregate.AggregateMetric BASAL_CALORIES_TOTAL;
     field public static final androidx.health.connect.client.records.BasalMetabolicRateRecord.Companion Companion;
   }
 
@@ -323,26 +323,18 @@
     field public static final String MEDIUM = "medium";
   }
 
-  public final class CyclingPedalingCadence {
-    ctor public CyclingPedalingCadence(java.time.Instant time, @FloatRange(from=0.0, to=10000.0) double revolutionsPerMinute);
-    method public double getRevolutionsPerMinute();
-    method public java.time.Instant getTime();
-    property public final double revolutionsPerMinute;
-    property public final java.time.Instant time;
-  }
-
-  public final class CyclingPedalingCadenceRecord implements androidx.health.connect.client.records.SeriesRecord {
-    ctor public CyclingPedalingCadenceRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+  public final class CyclingPedalingCadenceRecord implements androidx.health.connect.client.records.SeriesRecord {
+    ctor public CyclingPedalingCadenceRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.util.List getSamples();
+    method public java.util.List getSamples();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.util.List samples;
+    property public java.util.List samples;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
     field public static final androidx.health.connect.client.records.CyclingPedalingCadenceRecord.Companion Companion;
@@ -354,6 +346,14 @@
   public static final class CyclingPedalingCadenceRecord.Companion {
   }
 
+  public static final class CyclingPedalingCadenceRecord.Sample {
+    ctor public CyclingPedalingCadenceRecord.Sample(java.time.Instant time, @FloatRange(from=0.0, to=10000.0) double revolutionsPerMinute);
+    method public double getRevolutionsPerMinute();
+    method public java.time.Instant getTime();
+    property public final double revolutionsPerMinute;
+    property public final java.time.Instant time;
+  }
+
   public final class DistanceRecord implements androidx.health.connect.client.records.IntervalRecord {
     ctor public DistanceRecord(androidx.health.connect.client.units.Length distance, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.units.Length getDistance();
@@ -620,26 +620,18 @@
   public static final class FloorsClimbedRecord.Companion {
   }
 
-  public final class HeartRate {
-    ctor public HeartRate(java.time.Instant time, @IntRange(from=1L, to=300L) long beatsPerMinute);
-    method public long getBeatsPerMinute();
-    method public java.time.Instant getTime();
-    property public final long beatsPerMinute;
-    property public final java.time.Instant time;
-  }
-
-  public final class HeartRateRecord implements androidx.health.connect.client.records.SeriesRecord {
-    ctor public HeartRateRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+  public final class HeartRateRecord implements androidx.health.connect.client.records.SeriesRecord {
+    ctor public HeartRateRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.util.List getSamples();
+    method public java.util.List getSamples();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.util.List samples;
+    property public java.util.List samples;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
     field public static final androidx.health.connect.client.aggregate.AggregateMetric BPM_AVG;
@@ -652,6 +644,14 @@
   public static final class HeartRateRecord.Companion {
   }
 
+  public static final class HeartRateRecord.Sample {
+    ctor public HeartRateRecord.Sample(java.time.Instant time, @IntRange(from=1L, to=300L) long beatsPerMinute);
+    method public long getBeatsPerMinute();
+    method public java.time.Instant getTime();
+    property public final long beatsPerMinute;
+    property public final java.time.Instant time;
+  }
+
   public final class HeightRecord implements androidx.health.connect.client.records.InstantaneousRecord {
     ctor public HeightRecord(androidx.health.connect.client.units.Length height, java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public androidx.health.connect.client.units.Length getHeight();
@@ -1122,26 +1122,18 @@
     property public final java.time.Instant time;
   }
 
-  public final class StepsCadence {
-    ctor public StepsCadence(java.time.Instant time, @FloatRange(from=0.0, to=10000.0) double rate);
-    method public double getRate();
-    method public java.time.Instant getTime();
-    property public final double rate;
-    property public final java.time.Instant time;
-  }
-
-  public final class StepsCadenceRecord implements androidx.health.connect.client.records.SeriesRecord {
-    ctor public StepsCadenceRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+  public final class StepsCadenceRecord implements androidx.health.connect.client.records.SeriesRecord {
+    ctor public StepsCadenceRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, java.util.List samples, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public java.time.Instant getEndTime();
     method public java.time.ZoneOffset? getEndZoneOffset();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
-    method public java.util.List getSamples();
+    method public java.util.List getSamples();
     method public java.time.Instant getStartTime();
     method public java.time.ZoneOffset? getStartZoneOffset();
     property public java.time.Instant endTime;
     property public java.time.ZoneOffset? endZoneOffset;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
-    property public java.util.List samples;
+    property public java.util.List samples;
     property public java.time.Instant startTime;
     property public java.time.ZoneOffset? startZoneOffset;
     field public static final androidx.health.connect.client.records.StepsCadenceRecord.Companion Companion;
@@ -1153,6 +1145,14 @@
   public static final class StepsCadenceRecord.Companion {
   }
 
+  public static final class StepsCadenceRecord.Sample {
+    ctor public StepsCadenceRecord.Sample(java.time.Instant time, @FloatRange(from=0.0, to=10000.0) double rate);
+    method public double getRate();
+    method public java.time.Instant getTime();
+    property public final double rate;
+    property public final java.time.Instant time;
+  }
+
   public final class StepsRecord implements androidx.health.connect.client.records.IntervalRecord {
     ctor public StepsRecord(long count, java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.records.metadata.Metadata metadata);
     method public long getCount();
@@ -1353,15 +1353,15 @@
 package androidx.health.connect.client.request {
 
   public final class AggregateGroupByDurationRequest {
-    ctor public AggregateGroupByDurationRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, java.time.Duration timeRangeSlicer, optional java.util.List dataOriginFilter);
+    ctor public AggregateGroupByDurationRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, java.time.Duration timeRangeSlicer, optional java.util.Set dataOriginFilter);
   }
 
   public final class AggregateGroupByPeriodRequest {
-    ctor public AggregateGroupByPeriodRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, java.time.Period timeRangeSlicer, optional java.util.List dataOriginFilter);
+    ctor public AggregateGroupByPeriodRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, java.time.Period timeRangeSlicer, optional java.util.Set dataOriginFilter);
   }
 
   public final class AggregateRequest {
-    ctor public AggregateRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, optional java.util.List dataOriginFilter);
+    ctor public AggregateRequest(java.util.Set> metrics, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, optional java.util.Set dataOriginFilter);
   }
 
   public final class ChangesTokenRequest {
@@ -1369,7 +1369,7 @@
   }
 
   public final class ReadRecordsRequest {
-    ctor public ReadRecordsRequest(kotlin.reflect.KClass recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, optional java.util.List dataOriginFilter, optional boolean ascendingOrder, optional int pageSize, optional String? pageToken);
+    ctor public ReadRecordsRequest(kotlin.reflect.KClass recordType, androidx.health.connect.client.time.TimeRangeFilter timeRangeFilter, optional java.util.Set dataOriginFilter, optional boolean ascendingOrder, optional int pageSize, optional String? pageToken);
   }
 
 }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/aggregate/AggregationResult.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/aggregate/AggregationResult.kt
index 1c8ddae..0bd6c2b 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/aggregate/AggregationResult.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/aggregate/AggregationResult.kt
@@ -36,8 +36,8 @@
 internal constructor(
     internal val longValues: Map,
     internal val doubleValues: Map,
-    /** List of [DataOrigin]s that contributed to the aggregation result. */
-    public val dataOrigins: List
+    /** Set of [DataOrigin]s that contributed to the aggregation result. */
+    public val dataOrigins: Set
 ) {
 
     /**
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/aggregate/ProtoToAggregateDataRow.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/aggregate/ProtoToAggregateDataRow.kt
index db40939..150b677 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/aggregate/ProtoToAggregateDataRow.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/aggregate/ProtoToAggregateDataRow.kt
@@ -57,5 +57,5 @@
     AggregationResult(
         longValues = longValuesMap,
         doubleValues = doubleValuesMap,
-        dataOrigins = dataOriginsList.map { DataOrigin(it.applicationId) }
+        dataOrigins = dataOriginsList.mapTo(HashSet()) { DataOrigin(it.applicationId) }
     )
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
index f767c8b..41d9897 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
@@ -28,7 +28,6 @@
 import androidx.health.connect.client.records.BodyWaterMassRecord
 import androidx.health.connect.client.records.BoneMassRecord
 import androidx.health.connect.client.records.CervicalMucusRecord
-import androidx.health.connect.client.records.CyclingPedalingCadence
 import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
 import androidx.health.connect.client.records.DistanceRecord
 import androidx.health.connect.client.records.ElevationGainedRecord
@@ -37,7 +36,6 @@
 import androidx.health.connect.client.records.ExerciseRepetitionsRecord
 import androidx.health.connect.client.records.ExerciseSessionRecord
 import androidx.health.connect.client.records.FloorsClimbedRecord
-import androidx.health.connect.client.records.HeartRate
 import androidx.health.connect.client.records.HeartRateRecord
 import androidx.health.connect.client.records.HeartRateVariabilityDifferentialIndexRecord
 import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
@@ -64,7 +62,6 @@
 import androidx.health.connect.client.records.SleepSessionRecord
 import androidx.health.connect.client.records.SleepStageRecord
 import androidx.health.connect.client.records.SpeedRecord
-import androidx.health.connect.client.records.StepsCadence
 import androidx.health.connect.client.records.StepsCadenceRecord
 import androidx.health.connect.client.records.StepsRecord
 import androidx.health.connect.client.records.SwimmingStrokesRecord
@@ -171,7 +168,7 @@
                     endZoneOffset = endZoneOffset,
                     samples =
                         seriesValuesList.map { value ->
-                            CyclingPedalingCadence(
+                            CyclingPedalingCadenceRecord.Sample(
                                 time = Instant.ofEpochMilli(value.instantTimeMillis),
                                 revolutionsPerMinute = value.getDouble("rpm"),
                             )
@@ -186,7 +183,7 @@
                     endZoneOffset = endZoneOffset,
                     samples =
                         seriesValuesList.map { value ->
-                            HeartRate(
+                            HeartRateRecord.Sample(
                                 time = Instant.ofEpochMilli(value.instantTimeMillis),
                                 beatsPerMinute = value.getLong("bpm"),
                             )
@@ -357,7 +354,7 @@
                     endZoneOffset = endZoneOffset,
                     samples =
                         seriesValuesList.map { value ->
-                            StepsCadence(
+                            StepsCadenceRecord.Sample(
                                 time = Instant.ofEpochMilli(value.instantTimeMillis),
                                 rate = value.getDouble("rate"),
                             )
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/request/AggregateRequestToProto.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/request/AggregateRequestToProto.kt
index f3bfbd3..2f882268d 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/request/AggregateRequestToProto.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/impl/converters/request/AggregateRequestToProto.kt
@@ -52,5 +52,5 @@
         .setSlicePeriod(timeRangeSlicer.toString())
         .build()
 
-private fun List.toProtoList() =
+private fun Set.toProtoList() =
     this.map { DataProto.DataOrigin.newBuilder().setApplicationId(it.packageName).build() }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BasalMetabolicRateRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BasalMetabolicRateRecord.kt
index 156351e..4550a2b 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BasalMetabolicRateRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/BasalMetabolicRateRecord.kt
@@ -17,6 +17,7 @@
 
 import androidx.health.connect.client.aggregate.AggregateMetric
 import androidx.health.connect.client.records.metadata.Metadata
+import androidx.health.connect.client.units.Energy
 import androidx.health.connect.client.units.Power
 import java.time.Instant
 import java.time.ZoneOffset
@@ -66,12 +67,12 @@
          * [androidx.health.connect.client.aggregate.AggregationResult].
          */
         @JvmField
-        val BASAL_CALORIES_TOTAL: AggregateMetric =
+        val BASAL_CALORIES_TOTAL: AggregateMetric =
             AggregateMetric.doubleMetric(
                 dataTypeName = BASAL_CALORIES_TYPE_NAME,
                 aggregationType = AggregateMetric.AggregationType.TOTAL,
                 fieldName = ENERGY_FIELD_NAME,
-                mapper = Power::kilocaloriesPerDay,
+                mapper = Energy::calories,
             )
     }
 }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/CyclingPedalingCadenceRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/CyclingPedalingCadenceRecord.kt
index c8ba7be..0c87da2 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/CyclingPedalingCadenceRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/CyclingPedalingCadenceRecord.kt
@@ -33,9 +33,9 @@
     override val startZoneOffset: ZoneOffset?,
     override val endTime: Instant,
     override val endZoneOffset: ZoneOffset?,
-    override val samples: List,
+    override val samples: List,
     override val metadata: Metadata = Metadata.EMPTY,
-) : SeriesRecord {
+) : SeriesRecord {
 
     /*
      * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
@@ -89,44 +89,44 @@
          */
         @JvmField val RPM_MAX: AggregateMetric = doubleMetric(TYPE, MAXIMUM, RPM_FIELD)
     }
-}
 
-/**
- * Represents a single measurement of the cycling pedaling cadence.
- *
- * @param time The point in time when the measurement was taken.
- * @param revolutionsPerMinute Cycling revolutions per minute. Valid range: 0-10000.
- *
- * @see CyclingPedalingCadenceRecord
- */
-public class CyclingPedalingCadence(
-    val time: Instant,
-    @FloatRange(from = 0.0, to = 10_000.0) val revolutionsPerMinute: Double,
-) {
-
-    init {
-        requireNonNegative(value = revolutionsPerMinute, name = "revolutionsPerMinute")
-    }
-
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+    /**
+     * Represents a single measurement of the cycling pedaling cadence.
+     *
+     * @param time The point in time when the measurement was taken.
+     * @param revolutionsPerMinute Cycling revolutions per minute. Valid range: 0-10000.
+     *
+     * @see CyclingPedalingCadenceRecord
      */
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is CyclingPedalingCadence) return false
+    public class Sample(
+        val time: Instant,
+        @FloatRange(from = 0.0, to = 10_000.0) val revolutionsPerMinute: Double,
+    ) {
 
-        if (time != other.time) return false
-        if (revolutionsPerMinute != other.revolutionsPerMinute) return false
+        init {
+            requireNonNegative(value = revolutionsPerMinute, name = "revolutionsPerMinute")
+        }
 
-        return true
-    }
+        /*
+         * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+         */
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is Sample) return false
 
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
-     */
-    override fun hashCode(): Int {
-        var result = time.hashCode()
-        result = 31 * result + revolutionsPerMinute.hashCode()
-        return result
+            if (time != other.time) return false
+            if (revolutionsPerMinute != other.revolutionsPerMinute) return false
+
+            return true
+        }
+
+        /*
+         * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+         */
+        override fun hashCode(): Int {
+            var result = time.hashCode()
+            result = 31 * result + revolutionsPerMinute.hashCode()
+            return result
+        }
     }
 }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/HeartRateRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/HeartRateRecord.kt
index b16f820..bd488fe 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/HeartRateRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/HeartRateRecord.kt
@@ -27,9 +27,9 @@
     override val startZoneOffset: ZoneOffset?,
     override val endTime: Instant,
     override val endZoneOffset: ZoneOffset?,
-    override val samples: List,
+    override val samples: List,
     override val metadata: Metadata = Metadata.EMPTY,
-) : SeriesRecord {
+) : SeriesRecord {
 
     /*
      * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
@@ -100,44 +100,44 @@
         val MEASUREMENTS_COUNT: AggregateMetric =
             AggregateMetric.countMetric(HEART_RATE_TYPE_NAME)
     }
-}
 
-/**
- * Represents a single measurement of the heart rate.
- *
- * @param time The point in time when the measurement was taken.
- * @param beatsPerMinute Heart beats per minute. Validation range: 1-300.
- *
- * @see HeartRateRecord
- */
-public class HeartRate(
-    val time: Instant,
-    @androidx.annotation.IntRange(from = 1, to = 300) val beatsPerMinute: Long,
-) {
-
-    init {
-        requireNonNegative(value = beatsPerMinute, name = "beatsPerMinute")
-    }
-
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+    /**
+     * Represents a single measurement of the heart rate.
+     *
+     * @param time The point in time when the measurement was taken.
+     * @param beatsPerMinute Heart beats per minute. Validation range: 1-300.
+     *
+     * @see HeartRateRecord
      */
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is HeartRate) return false
+    public class Sample(
+        val time: Instant,
+        @androidx.annotation.IntRange(from = 1, to = 300) val beatsPerMinute: Long,
+    ) {
 
-        if (time != other.time) return false
-        if (beatsPerMinute != other.beatsPerMinute) return false
+        init {
+            requireNonNegative(value = beatsPerMinute, name = "beatsPerMinute")
+        }
 
-        return true
-    }
+        /*
+         * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+         */
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is Sample) return false
 
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
-     */
-    override fun hashCode(): Int {
-        var result = time.hashCode()
-        result = 31 * result + beatsPerMinute.hashCode()
-        return result
+            if (time != other.time) return false
+            if (beatsPerMinute != other.beatsPerMinute) return false
+
+            return true
+        }
+
+        /*
+         * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+         */
+        override fun hashCode(): Int {
+            var result = time.hashCode()
+            result = 31 * result + beatsPerMinute.hashCode()
+            return result
+        }
     }
 }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/MealType.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/MealType.kt
index c4a2852..40299d1 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/MealType.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/MealType.kt
@@ -20,9 +20,13 @@
 /** Type of meal. */
 public object MealType {
     const val UNKNOWN = "unknown"
+    /** Use this for the first meal of the day, usually the morning meal. */
     const val BREAKFAST = "breakfast"
+    /** Use this for the noon meal. */
     const val LUNCH = "lunch"
+    /** Use this for last meal of the day, usually the evening meal. */
     const val DINNER = "dinner"
+    /** Any meal outside of the usual three meals per day. */
     const val SNACK = "snack"
 }
 
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/StepsCadenceRecord.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/StepsCadenceRecord.kt
index b10ccfe..d7488b6 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/records/StepsCadenceRecord.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/records/StepsCadenceRecord.kt
@@ -31,9 +31,9 @@
     override val startZoneOffset: ZoneOffset?,
     override val endTime: Instant,
     override val endZoneOffset: ZoneOffset?,
-    override val samples: List,
+    override val samples: List,
     override val metadata: Metadata = Metadata.EMPTY,
-) : SeriesRecord {
+) : SeriesRecord {
 
     /*
      * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
@@ -87,42 +87,42 @@
          */
         @JvmField val RATE_MAX: AggregateMetric = doubleMetric(TYPE, MAXIMUM, RATE_FIELD)
     }
-}
 
-/**
- * Represents a single measurement of the steps cadence.
- *
- * @param time The point in time when the measurement was taken.
- * @param rate Rate in steps per minute. Valid range: 0-10000.
- */
-class StepsCadence(
-    val time: Instant,
-    @FloatRange(from = 0.0, to = 10_000.0) val rate: Double,
-) {
-
-    init {
-        requireNonNegative(value = rate, name = "rate")
-    }
-
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+    /**
+     * Represents a single measurement of the steps cadence.
+     *
+     * @param time The point in time when the measurement was taken.
+     * @param rate Rate in steps per minute. Valid range: 0-10000.
      */
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is StepsCadence) return false
+    class Sample(
+        val time: Instant,
+        @FloatRange(from = 0.0, to = 10_000.0) val rate: Double,
+    ) {
 
-        if (time != other.time) return false
-        if (rate != other.rate) return false
+        init {
+            requireNonNegative(value = rate, name = "rate")
+        }
 
-        return true
-    }
+        /*
+         * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+         */
+        override fun equals(other: Any?): Boolean {
+            if (this === other) return true
+            if (other !is Sample) return false
 
-    /*
-     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
-     */
-    override fun hashCode(): Int {
-        var result = time.hashCode()
-        result = 31 * result + rate.hashCode()
-        return result
+            if (time != other.time) return false
+            if (rate != other.rate) return false
+
+            return true
+        }
+
+        /*
+         * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+         */
+        override fun hashCode(): Int {
+            var result = time.hashCode()
+            result = 31 * result + rate.hashCode()
+            return result
+        }
     }
 }
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/request/AggregateGroupByDurationRequest.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/request/AggregateGroupByDurationRequest.kt
index 8c09819..7650981 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/request/AggregateGroupByDurationRequest.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/request/AggregateGroupByDurationRequest.kt
@@ -24,15 +24,19 @@
  * Request object to read time bucketed aggregations for given [AggregateMetric]s in Android Health
  * Platform.
  *
+ * [timeRangeSlicer] contains a [Duration] of fixed physical time intervals, such as per hour, per
+ * ten minutes or so. Prefer [AggregateGroupByPeriodRequest], if you would like variable length time
+ * intervals, such as per day, which may or may not include DST (23 or 25 hour).
+ *
  * @param metrics Set of [AggregateMetric]s to aggregate, such as `Steps::STEPS_COUNT_TOTAL`.
  * @param timeRangeFilter The [TimeRangeFilter] to read from.
  * @param timeRangeSlicer The bucket size of each returned aggregate row. [timeRangeFilter] will be
  * sliced into several equal-sized time buckets (except for the last one).
- * @param dataOriginFilter List of [DataOrigin]s to read from, or empty for no filter.
+ * @param dataOriginFilter Set of [DataOrigin]s to read from, or empty for no filter.
  */
 class AggregateGroupByDurationRequest(
     internal val metrics: Set>,
     internal val timeRangeFilter: TimeRangeFilter,
     internal val timeRangeSlicer: Duration,
-    internal val dataOriginFilter: List = emptyList(),
+    internal val dataOriginFilter: Set = emptySet(),
 )
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/request/AggregateGroupByPeriodRequest.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/request/AggregateGroupByPeriodRequest.kt
index bae6286..c95d4e3 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/request/AggregateGroupByPeriodRequest.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/request/AggregateGroupByPeriodRequest.kt
@@ -24,15 +24,19 @@
  * Request object to read time bucketed aggregations for given [AggregateMetric]s in Android Health
  * Platform.
  *
+ * [timeRangeSlicer] contains a [Period] of variable length time intervals, such as per day, which
+ * may be anywhere between 23, 24, or 25 hour. Use [AggregateGroupByDurationRequest] if time slice
+ * is of fixed intervals, such as an hour, every ten minutes.
+ *
  * @param metrics Set of [AggregateMetric]s to aggregate, such as `Steps::STEPS_COUNT_TOTAL`.
  * @param timeRangeFilter The [TimeRangeFilter] to read from.
  * @param timeRangeSlicer The bucket size of each returned aggregate row. [timeRangeFilter] will be
  * sliced into several equal-sized time buckets (except for the last one).
- * @param dataOriginFilter List of [DataOrigin]s to read from, or empty for no filter.
+ * @param dataOriginFilter Set of [DataOrigin]s to read from, or empty for no filter.
  */
 class AggregateGroupByPeriodRequest(
     internal val metrics: Set>,
     internal val timeRangeFilter: TimeRangeFilter,
     internal val timeRangeSlicer: Period,
-    internal val dataOriginFilter: List = emptyList(),
+    internal val dataOriginFilter: Set = emptySet(),
 )
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/request/AggregateRequest.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/request/AggregateRequest.kt
index 10ddc27..f578c8f 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/request/AggregateRequest.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/request/AggregateRequest.kt
@@ -24,10 +24,10 @@
  *
  * @param metrics Set of [AggregateMetric]s to aggregate, such as `Steps::STEPS_COUNT_TOTAL`.
  * @param timeRangeFilter The [TimeRangeFilter] to read from.
- * @param dataOriginFilter List of [DataOrigin]s to read from, or empty for no filter.
+ * @param dataOriginFilter Set of [DataOrigin]s to read from, or empty for no filter.
  */
 class AggregateRequest(
     internal val metrics: Set>,
     internal val timeRangeFilter: TimeRangeFilter,
-    internal val dataOriginFilter: List = emptyList(),
+    internal val dataOriginFilter: Set = emptySet(),
 )
diff --git a/health/health-connect-client/src/main/java/androidx/health/connect/client/request/ReadRecordsRequest.kt b/health/health-connect-client/src/main/java/androidx/health/connect/client/request/ReadRecordsRequest.kt
index cf16e51..e328742 100644
--- a/health/health-connect-client/src/main/java/androidx/health/connect/client/request/ReadRecordsRequest.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/connect/client/request/ReadRecordsRequest.kt
@@ -47,7 +47,7 @@
 public class ReadRecordsRequest(
     internal val recordType: KClass,
     internal val timeRangeFilter: TimeRangeFilter,
-    internal val dataOriginFilter: List = emptyList(),
+    internal val dataOriginFilter: Set = emptySet(),
     internal val ascendingOrder: Boolean = true,
     internal val pageSize: Int = 1000,
     internal val pageToken: String? = null,
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/data/ProtoParcelable.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/data/ProtoParcelable.kt
index 1a74b65..b53d47d 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/data/ProtoParcelable.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/data/ProtoParcelable.kt
@@ -66,7 +66,7 @@
          * Constructs and returns a [Creator] based on the provided [parser] accepting a [ByteArray]
          * .
          */
-        inline fun > newCreator(
+        internal inline fun > newCreator(
             crossinline parser: (ByteArray) -> U
         ): Creator {
             return object : Creator {
@@ -93,10 +93,10 @@
 }
 
 /** Flag marking that a proto is stored as an in-place `byte[]` array. */
-const val STORE_IN_PLACE = 0
+internal const val STORE_IN_PLACE = 0
 
 /** Flag marking that a proto is stored in [SharedMemory]. */
-const val STORE_SHARED_MEMORY = 1
+internal const val STORE_SHARED_MEMORY = 1
 
 /** Maximum size of a proto stored as an in-place `byte[]` array (16 KiB). */
 private const val MAX_IN_PLACE_SIZE = 16 * 1024
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/error/ErrorStatusConverter.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/error/ErrorStatusConverter.kt
index 6e6af7c..8a72c1d 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/error/ErrorStatusConverter.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/error/ErrorStatusConverter.kt
@@ -22,7 +22,7 @@
 import java.io.IOException
 import java.lang.IllegalArgumentException
 
-val errorCodeExceptionMap =
+internal val errorCodeExceptionMap =
     mapOf(
         ErrorCode.PROVIDER_NOT_INSTALLED to java.lang.UnsupportedOperationException::class,
         ErrorCode.PROVIDER_NOT_ENABLED to java.lang.UnsupportedOperationException::class,
@@ -41,7 +41,7 @@
     )
 
 @Suppress("ObsoleteSdkInt") // We want to target lower down to 14 in the future.
-fun ErrorStatus.toException(): Exception {
+internal fun ErrorStatus.toException(): Exception {
     errorCodeExceptionMap[this.errorCode]?.let {
         return when (it) {
             SecurityException::class -> SecurityException(this.errorMessage)
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/permission/foregroundstate/package-info.java b/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/permission/foregroundstate/package-info.java
new file mode 100644
index 0000000..5c17428
--- /dev/null
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/permission/foregroundstate/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** @hide */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.health.platform.client.impl.permission.foregroundstate;
+
+import androidx.annotation.RestrictTo;
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/permission/token/package-info.java b/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/permission/token/package-info.java
new file mode 100644
index 0000000..7dc9361
--- /dev/null
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/permission/token/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** @hide */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.health.platform.client.impl.permission.token;
+
+import androidx.annotation.RestrictTo;
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/sdkservice/package-info.java b/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/sdkservice/package-info.java
new file mode 100644
index 0000000..849ce71
--- /dev/null
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/impl/sdkservice/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** @hide */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.health.platform.client.impl.sdkservice;
+
+import androidx.annotation.RestrictTo;
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/permission/Permission.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/permission/Permission.kt
index 919e286..359ea2a 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/permission/Permission.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/permission/Permission.kt
@@ -19,7 +19,11 @@
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.PermissionProto
 
-/** Internal parcelable wrapper over proto object. */
+/**
+ * Internal parcelable wrapper over proto object.
+ *
+ * @suppress
+ */
 class Permission(override val proto: PermissionProto.Permission) :
     ProtoParcelable() {
 
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/AggregateDataRequest.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/AggregateDataRequest.kt
index 5aa463f..f2eaf86 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/AggregateDataRequest.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/AggregateDataRequest.kt
@@ -13,13 +13,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package androidx.health.platform.client.request
 
 import android.os.Parcelable
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.RequestProto
 
-/** Internal wrapper to help transfer protos over ipc. */
+/**
+ * Internal wrapper to help transfer protos over ipc.
+ * @suppress
+ */
 class AggregateDataRequest(override val proto: RequestProto.AggregateDataRequest) :
     ProtoParcelable() {
 
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/DeleteDataRangeRequest.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/DeleteDataRangeRequest.kt
index c203971..e0803f9 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/DeleteDataRangeRequest.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/DeleteDataRangeRequest.kt
@@ -19,7 +19,10 @@
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.RequestProto
 
-/** Internal parcelable for IPC calls. */
+/**
+ * Internal parcelable for IPC calls.
+ * @suppress
+ */
 class DeleteDataRangeRequest(override val proto: RequestProto.DeleteDataRangeRequest) :
     ProtoParcelable() {
 
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/DeleteDataRequest.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/DeleteDataRequest.kt
index b144b84..8421c162 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/DeleteDataRequest.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/DeleteDataRequest.kt
@@ -13,13 +13,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package androidx.health.platform.client.request
 
 import android.os.Parcelable
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.RequestProto
 
-/** Internal parcelable for IPC calls. */
+/**
+ * Internal parcelable for IPC calls.
+ *
+ * @suppress
+ */
 class DeleteDataRequest(
     val uids: List,
     val clientIds: List
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/GetChangesRequest.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/GetChangesRequest.kt
index ddc7591..604c5ed 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/GetChangesRequest.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/GetChangesRequest.kt
@@ -19,6 +19,7 @@
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.RequestProto
 
+/** @suppress */
 class GetChangesRequest(override val proto: RequestProto.GetChangesRequest) :
     ProtoParcelable() {
 
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/GetChangesTokenRequest.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/GetChangesTokenRequest.kt
index 45cd5d4..57f4316 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/GetChangesTokenRequest.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/GetChangesTokenRequest.kt
@@ -13,12 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package androidx.health.platform.client.request
 
 import android.os.Parcelable
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.RequestProto
 
+/** @suppress */
 class GetChangesTokenRequest(override val proto: RequestProto.GetChangesTokenRequest) :
     ProtoParcelable() {
 
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/ReadDataRangeRequest.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/ReadDataRangeRequest.kt
index 8a8d0f8..a6f0203 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/ReadDataRangeRequest.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/ReadDataRangeRequest.kt
@@ -13,12 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package androidx.health.platform.client.request
 
 import android.os.Parcelable
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.RequestProto
 
+/** @suppress */
 class ReadDataRangeRequest(override val proto: RequestProto.ReadDataRangeRequest) :
     ProtoParcelable() {
 
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/ReadDataRequest.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/ReadDataRequest.kt
index 561bd42..5d70427 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/ReadDataRequest.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/ReadDataRequest.kt
@@ -13,13 +13,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package androidx.health.platform.client.request
 
 import android.os.Parcelable
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.RequestProto
 
-/** Internal parcelable for IPC calls. */
+/**
+ * Internal parcelable for IPC calls.
+ *
+ * @suppress
+ */
 class ReadDataRequest(override val proto: RequestProto.ReadDataRequest) :
     ProtoParcelable() {
 
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/RequestContext.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/RequestContext.kt
index 1bfe050..48ffebe2 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/RequestContext.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/RequestContext.kt
@@ -13,13 +13,17 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package androidx.health.platform.client.request
 
 import android.os.Parcelable
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.RequestProto
 
-/** Data object holding context data for IPC calls. */
+/**
+ * Data object holding context data for IPC calls.
+ * @suppress
+ */
 class RequestContext(
     val callingPackage: String,
     val sdkVersion: Int,
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/UpsertDataRequest.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/UpsertDataRequest.kt
index fa23fcc..9851795 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/request/UpsertDataRequest.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/request/UpsertDataRequest.kt
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package androidx.health.platform.client.request
 
 import android.os.Parcelable
@@ -20,7 +21,11 @@
 import androidx.health.platform.client.proto.DataProto
 import androidx.health.platform.client.proto.RequestProto
 
-/** Internal parcelable for IPC calls. */
+/**
+ * Internal parcelable for IPC calls.
+ *
+ * @suppress
+ */
 class UpsertDataRequest(val dataPoints: List) :
     ProtoParcelable() {
     override val proto: RequestProto.UpsertDataRequest
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/response/AggregateDataResponse.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/response/AggregateDataResponse.kt
index 86320ce..00f9356 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/response/AggregateDataResponse.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/response/AggregateDataResponse.kt
@@ -13,13 +13,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package androidx.health.platform.client.response
 
 import android.os.Parcelable
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.ResponseProto
 
-/** Internal wrapper to help transfer protos over ipc. */
+/**
+ * Internal wrapper to help transfer protos over ipc.
+ *
+ * @suppress
+ */
 class AggregateDataResponse(override val proto: ResponseProto.AggregateDataResponse) :
     ProtoParcelable() {
 
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/response/GetChangesResponse.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/response/GetChangesResponse.kt
index 5c1c7bf..59c684c3 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/response/GetChangesResponse.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/response/GetChangesResponse.kt
@@ -13,12 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package androidx.health.platform.client.response
 
 import android.os.Parcelable
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.ResponseProto
 
+/** @suppress */
 class GetChangesResponse(override val proto: ResponseProto.GetChangesResponse) :
     ProtoParcelable() {
 
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/response/GetChangesTokenResponse.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/response/GetChangesTokenResponse.kt
index b2f6069..7d232a6 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/response/GetChangesTokenResponse.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/response/GetChangesTokenResponse.kt
@@ -13,12 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package androidx.health.platform.client.response
 
 import android.os.Parcelable
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.ResponseProto
 
+/** @suppress */
 class GetChangesTokenResponse(override val proto: ResponseProto.GetChangesTokenResponse) :
     ProtoParcelable() {
 
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/response/InsertDataResponse.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/response/InsertDataResponse.kt
index 99f33c7..af5d80d 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/response/InsertDataResponse.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/response/InsertDataResponse.kt
@@ -13,12 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package androidx.health.platform.client.response
 
 import android.os.Parcelable
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.ResponseProto
 
+/** @suppress */
 class InsertDataResponse(val dataPointUids: List) :
     ProtoParcelable() {
     override val proto: ResponseProto.InsertDataResponse
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/response/ReadDataRangeResponse.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/response/ReadDataRangeResponse.kt
index 9ec63bc..97652ab 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/response/ReadDataRangeResponse.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/response/ReadDataRangeResponse.kt
@@ -13,12 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package androidx.health.platform.client.response
 
 import android.os.Parcelable
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.ResponseProto
 
+/** @suppress */
 class ReadDataRangeResponse(override val proto: ResponseProto.ReadDataRangeResponse) :
     ProtoParcelable() {
 
diff --git a/health/health-connect-client/src/main/java/androidx/health/platform/client/response/ReadDataResponse.kt b/health/health-connect-client/src/main/java/androidx/health/platform/client/response/ReadDataResponse.kt
index 5966c1e..5f26162 100644
--- a/health/health-connect-client/src/main/java/androidx/health/platform/client/response/ReadDataResponse.kt
+++ b/health/health-connect-client/src/main/java/androidx/health/platform/client/response/ReadDataResponse.kt
@@ -19,6 +19,7 @@
 import androidx.health.platform.client.impl.data.ProtoParcelable
 import androidx.health.platform.client.proto.ResponseProto
 
+/** @suppress */
 class ReadDataResponse(override val proto: ResponseProto.ReadDataResponse) :
     ProtoParcelable() {
 
diff --git a/health/health-connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt b/health/health-connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
index ec02294..9df3d3c 100644
--- a/health/health-connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
+++ b/health/health-connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
@@ -22,10 +22,9 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.common.truth.Truth.assertThat
-import java.lang.IllegalStateException
-import java.lang.UnsupportedOperationException
 import org.junit.Assert.assertThrows
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.Shadows
@@ -43,6 +42,7 @@
         context = ApplicationProvider.getApplicationContext()
     }
 
+    @Ignore // b/238635208
     @Test
     fun noBackingImplementation_unavailable() {
         val packageManager = context.packageManager
@@ -54,6 +54,7 @@
         }
     }
 
+    @Ignore // b/238635208
     @Test
     fun backingImplementation_notEnabled_unavailable() {
         installPackage(context, PROVIDER_PACKAGE_NAME, enabled = false)
@@ -64,6 +65,7 @@
         }
     }
 
+    @Ignore // b/238635208
     @Test
     fun backingImplementation_enabled_isAvailable() {
         installPackage(context, PROVIDER_PACKAGE_NAME, enabled = true)
diff --git a/health/health-connect-client/src/test/java/androidx/health/connect/client/aggregate/AggregationResultGroupedByPeriodTest.kt b/health/health-connect-client/src/test/java/androidx/health/connect/client/aggregate/AggregationResultGroupedByPeriodTest.kt
index 359c6ef..7bb1ebe 100644
--- a/health/health-connect-client/src/test/java/androidx/health/connect/client/aggregate/AggregationResultGroupedByPeriodTest.kt
+++ b/health/health-connect-client/src/test/java/androidx/health/connect/client/aggregate/AggregationResultGroupedByPeriodTest.kt
@@ -28,7 +28,7 @@
     fun constructor_endTimeNotAfterStartTime_throws() {
         assertThrows(IllegalArgumentException::class.java) {
             AggregationResultGroupedByPeriod(
-                result = AggregationResult(mapOf(), mapOf(), listOf()),
+                result = AggregationResult(mapOf(), mapOf(), setOf()),
                 startTime = LocalDateTime.parse("2022-02-22T20:22:02"),
                 endTime = LocalDateTime.parse("2022-02-11T20:22:02"),
             )
@@ -36,14 +36,14 @@
 
         assertThrows(IllegalArgumentException::class.java) {
             AggregationResultGroupedByPeriod(
-                result = AggregationResult(mapOf(), mapOf(), listOf()),
+                result = AggregationResult(mapOf(), mapOf(), setOf()),
                 startTime = LocalDateTime.parse("2022-02-11T20:22:02"),
                 endTime = LocalDateTime.parse("2022-02-11T20:22:02"),
             )
         }
 
         AggregationResultGroupedByPeriod(
-            result = AggregationResult(mapOf(), mapOf(), listOf()),
+            result = AggregationResult(mapOf(), mapOf(), setOf()),
             startTime = LocalDateTime.parse("2022-02-11T20:22:02"),
             endTime = LocalDateTime.parse("2022-02-22T20:22:02"),
         )
diff --git a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/aggregate/AggregationResultConverterTest.kt b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/aggregate/AggregationResultConverterTest.kt
index 414a2de..571db2b 100644
--- a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/aggregate/AggregationResultConverterTest.kt
+++ b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/aggregate/AggregationResultConverterTest.kt
@@ -46,7 +46,7 @@
                 AggregationResult(
                     longValues = mapOf(Pair("longKey", 567L)),
                     doubleValues = mapOf(Pair("doubleKey", 123.4)),
-                    dataOrigins = listOf(DataOrigin("testApp")),
+                    dataOrigins = setOf(DataOrigin("testApp")),
                 )
             )
     }
@@ -73,7 +73,7 @@
                         AggregationResult(
                             longValues = mapOf(Pair("longKey", 567L)),
                             doubleValues = mapOf(Pair("doubleKey", 123.4)),
-                            dataOrigins = listOf(DataOrigin("testApp")),
+                            dataOrigins = setOf(DataOrigin("testApp")),
                         ),
                     startTime = Instant.ofEpochMilli(1111),
                     endTime = Instant.ofEpochMilli(9999),
@@ -129,7 +129,7 @@
                         AggregationResult(
                             longValues = mapOf(Pair("longKey", 567L)),
                             doubleValues = mapOf(Pair("doubleKey", 123.4)),
-                            dataOrigins = listOf(DataOrigin("testApp")),
+                            dataOrigins = setOf(DataOrigin("testApp")),
                         ),
                     startTime = LocalDateTime.parse("2022-02-11T20:22:02"),
                     endTime = LocalDateTime.parse("2022-02-22T20:22:02"),
diff --git a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
index 2679086d..1510f7b 100644
--- a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
+++ b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
@@ -29,7 +29,6 @@
 import androidx.health.connect.client.records.CervicalMucusRecord
 import androidx.health.connect.client.records.CervicalMucusRecord.Appearance
 import androidx.health.connect.client.records.CervicalMucusRecord.Sensation
-import androidx.health.connect.client.records.CyclingPedalingCadence
 import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
 import androidx.health.connect.client.records.DistanceRecord
 import androidx.health.connect.client.records.ElevationGainedRecord
@@ -40,7 +39,6 @@
 import androidx.health.connect.client.records.ExerciseSessionRecord
 import androidx.health.connect.client.records.ExerciseSessionRecord.ExerciseType
 import androidx.health.connect.client.records.FloorsClimbedRecord
-import androidx.health.connect.client.records.HeartRate
 import androidx.health.connect.client.records.HeartRateRecord
 import androidx.health.connect.client.records.HeartRateVariabilityDifferentialIndexRecord
 import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
@@ -70,7 +68,6 @@
 import androidx.health.connect.client.records.SleepStageRecord
 import androidx.health.connect.client.records.SleepStageRecord.StageType
 import androidx.health.connect.client.records.SpeedRecord
-import androidx.health.connect.client.records.StepsCadence
 import androidx.health.connect.client.records.StepsCadenceRecord
 import androidx.health.connect.client.records.StepsRecord
 import androidx.health.connect.client.records.SwimmingStrokesRecord
@@ -277,11 +274,11 @@
                 endZoneOffset = END_ZONE_OFFSET,
                 samples =
                     listOf(
-                        CyclingPedalingCadence(
+                        CyclingPedalingCadenceRecord.Sample(
                             time = START_TIME,
                             revolutionsPerMinute = 1.0,
                         ),
-                        CyclingPedalingCadence(
+                        CyclingPedalingCadenceRecord.Sample(
                             time = START_TIME,
                             revolutionsPerMinute = 2.0,
                         ),
@@ -303,15 +300,15 @@
                 endZoneOffset = END_ZONE_OFFSET,
                 samples =
                     listOf(
-                        HeartRate(
+                        HeartRateRecord.Sample(
                             time = START_TIME,
                             beatsPerMinute = 100L,
                         ),
-                        HeartRate(
+                        HeartRateRecord.Sample(
                             time = START_TIME,
                             beatsPerMinute = 110L,
                         ),
-                        HeartRate(
+                        HeartRateRecord.Sample(
                             time = START_TIME,
                             beatsPerMinute = 120L,
                         ),
@@ -637,11 +634,11 @@
                 endZoneOffset = END_ZONE_OFFSET,
                 samples =
                     listOf(
-                        StepsCadence(
+                        StepsCadenceRecord.Sample(
                             time = START_TIME,
                             rate = 1.0,
                         ),
-                        StepsCadence(
+                        StepsCadenceRecord.Sample(
                             time = START_TIME,
                             rate = 2.0,
                         ),
diff --git a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/request/AggregateRequestConverterTest.kt b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/request/AggregateRequestConverterTest.kt
index 5b30746..b742e18 100644
--- a/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/request/AggregateRequestConverterTest.kt
+++ b/health/health-connect-client/src/test/java/androidx/health/connect/client/impl/converters/request/AggregateRequestConverterTest.kt
@@ -40,7 +40,7 @@
         .setFieldName("count")
 private val TIME_RANGE_FILTER =
     TimeRangeFilter.between(Instant.ofEpochMilli(123), Instant.ofEpochMilli(456))
-private val DATA_ORIGIN_FILTER = listOf(DataOrigin("testAppName"))
+private val DATA_ORIGIN_FILTER = setOf(DataOrigin("testAppName"))
 
 @RunWith(AndroidJUnit4::class)
 class AggregateRequestConverterTest {
@@ -105,6 +105,6 @@
             )
     }
 
-    private fun List.toProtoList() =
+    private fun Set.toProtoList() =
         this.map { DataProto.DataOrigin.newBuilder().setApplicationId(it.packageName).build() }
 }
diff --git a/health/health-connect-client/src/test/java/androidx/health/platform/client/impl/data/ProtoParcelableTest.kt b/health/health-connect-client/src/test/java/androidx/health/platform/client/impl/data/ProtoParcelableTest.kt
index b295712..dd49758 100644
--- a/health/health-connect-client/src/test/java/androidx/health/platform/client/impl/data/ProtoParcelableTest.kt
+++ b/health/health-connect-client/src/test/java/androidx/health/platform/client/impl/data/ProtoParcelableTest.kt
@@ -21,6 +21,7 @@
 import androidx.health.platform.client.proto.BytesValue
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -36,6 +37,7 @@
         assertThat(parcelAndRead(protoParcelable).proto).isEqualTo(protoParcelable.proto)
     }
 
+    @Ignore // b/238635208
     @Test
     fun storeInSharedMemory() {
         // Big enough that it will be stored in shared memory.
diff --git a/libraryversions.toml b/libraryversions.toml
index bce6a79..80909c7 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -3,7 +3,7 @@
 ADS_IDENTIFIER = "1.0.0-alpha05"
 ANNOTATION = "1.4.0-rc01"
 ANNOTATION_EXPERIMENTAL = "1.3.0-alpha01"
-APPCOMPAT = "1.5.0-beta01"
+APPCOMPAT = "1.5.0-rc01"
 APPSEARCH = "1.0.0-alpha05"
 ARCH_CORE = "2.2.0-alpha01"
 ASYNCLAYOUTINFLATER = "1.1.0-alpha01"
@@ -45,7 +45,7 @@
 DYNAMICANIMATION = "1.1.0-alpha04"
 DYNAMICANIMATION_KTX = "1.0.0-alpha04"
 EMOJI = "1.2.0-alpha03"
-EMOJI2 = "1.2.0-beta01"
+EMOJI2 = "1.2.0-rc01"
 ENTERPRISE = "1.1.0-rc01"
 EXIFINTERFACE = "1.4.0-alpha01"
 FRAGMENT = "1.6.0-alpha01"
@@ -86,7 +86,7 @@
 RECYCLERVIEW = "1.3.0-beta02"
 RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
 REMOTECALLBACK = "1.0.0-alpha02"
-RESOURCEINSPECTION = "1.1.0-beta01"
+RESOURCEINSPECTION = "1.1.0-alpha01"
 ROOM = "2.5.0-alpha03"
 SAVEDSTATE = "1.3.0-alpha01"
 SECURITY = "1.1.0-alpha04"
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaverTest.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaverTest.kt
index 0dc453e..1463fc7 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaverTest.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaverTest.kt
@@ -19,9 +19,11 @@
 import androidx.activity.compose.setContent
 import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.referentialEqualityPolicy
 import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.SnapshotMutableState
 import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
@@ -72,6 +74,60 @@
         assertThat(array).isEqualTo(intArrayOf(1))
     }
 
+    private class CustomStateHolder(
+        initialValue: Int
+    ) {
+        var value by mutableStateOf(initialValue)
+
+        companion object {
+            val Saver: Saver = Saver(
+                save = { it.value },
+                restore = { CustomStateHolder(it) }
+            )
+        }
+    }
+
+    @OptIn(SavedStateHandleSaveableApi::class)
+    @Test
+    fun customStateHolder_simpleRestore() {
+        var stateHolder: CustomStateHolder? = null
+        activityTestRuleScenario.scenario.onActivity { activity ->
+            activity.setContent {
+                val viewModel = viewModel(activity)
+                stateHolder = viewModel.savedStateHandle.saveable(
+                    key = "key",
+                    saver = CustomStateHolder.Saver
+                ) {
+                    CustomStateHolder(0)
+                }
+            }
+        }
+
+        assertThat(stateHolder?.value).isEqualTo(0)
+
+        activityTestRuleScenario.scenario.onActivity {
+            stateHolder!!.value = 1
+            // we null it to ensure recomposition happened
+            stateHolder = null
+        }
+
+        activityTestRuleScenario.scenario.recreate()
+
+        activityTestRuleScenario.scenario.onActivity { activity ->
+            activity.setContent {
+                val viewModel = viewModel(activity)
+                stateHolder = viewModel.savedStateHandle.saveable(
+                    key = "key",
+                    saver = CustomStateHolder.Saver
+                ) {
+                    CustomStateHolder(0)
+                }
+            }
+        }
+
+        assertThat(stateHolder?.value).isEqualTo(1)
+    }
+
     private data class CustomState(
         val value: Int
     ) {
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/main/java/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaver.kt b/lifecycle/lifecycle-viewmodel-compose/src/main/java/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaver.kt
index 51e909e..8c70d69 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/main/java/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaver.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/main/java/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaver.kt
@@ -62,7 +62,7 @@
     // Hook up saving the state to the SavedStateHandle
     setSavedStateProvider(key) {
         bundleOf("value" to with(saver) {
-            SaverScope { validateValue(value) }.save(value)
+            SaverScope(::validateValue).save(value)
         })
     }
     return value
diff --git a/lifecycle/settings.gradle b/lifecycle/settings.gradle
index 573845c..cda538c 100644
--- a/lifecycle/settings.gradle
+++ b/lifecycle/settings.gradle
@@ -28,7 +28,7 @@
     setupPlayground("..")
     selectProjectsFromAndroidX({ name ->
         if (name.startsWith(":lifecycle")) return true
-        if (name == ":annotation:annotation") return true
+        if (name.startsWith(":annotation")) return true
         if (name == ":internal-testutils-runtime") return true
         if (name == ":internal-testutils-truth") return true
         if (isNeededForComposePlayground(name)) return true
diff --git a/metrics/integration-tests/janktest/src/main/java/androidx/metrics/performance/janktest/JankLoggingActivity.kt b/metrics/integration-tests/janktest/src/main/java/androidx/metrics/performance/janktest/JankLoggingActivity.kt
index 9058da4..37c9236 100644
--- a/metrics/integration-tests/janktest/src/main/java/androidx/metrics/performance/janktest/JankLoggingActivity.kt
+++ b/metrics/integration-tests/janktest/src/main/java/androidx/metrics/performance/janktest/JankLoggingActivity.kt
@@ -28,8 +28,6 @@
 import androidx.navigation.ui.AppBarConfiguration
 import androidx.navigation.ui.navigateUp
 import androidx.navigation.ui.setupActionBarWithNavController
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.asExecutor
 
 /**
  * This activity shows the basic usage of JankStats, from creating and enabling it to track
@@ -49,8 +47,7 @@
         setContentView(binding.root)
 
         val metricsStateHolder = PerformanceMetricsState.getForHierarchy(binding.root)
-        jankStats = JankStats.createAndTrack(window, Dispatchers.Default.asExecutor(),
-            jankFrameListener)
+        jankStats = JankStats.createAndTrack(window, jankFrameListener)
         metricsStateHolder.state?.addState("Activity", javaClass.simpleName)
 
         setSupportActionBar(binding.toolbar)
@@ -61,8 +58,8 @@
     }
 
     object jankFrameListener : JankStats.OnFrameListener {
-        override fun onFrame(frameData: FrameData) {
-            println("JankStats.OnFrameListener: $frameData")
+        override fun onFrame(volatileFrameData: FrameData) {
+            println("JankStats.OnFrameListener: $volatileFrameData")
         }
     }
 
diff --git a/metrics/integration-tests/janktest/src/main/java/androidx/metrics/performance/janktest/JankStatsAggregator.kt b/metrics/integration-tests/janktest/src/main/java/androidx/metrics/performance/janktest/JankStatsAggregator.kt
index f676976..9aa0bf0 100644
--- a/metrics/integration-tests/janktest/src/main/java/androidx/metrics/performance/janktest/JankStatsAggregator.kt
+++ b/metrics/integration-tests/janktest/src/main/java/androidx/metrics/performance/janktest/JankStatsAggregator.kt
@@ -43,10 +43,12 @@
 ) {
 
     private val listener = object : JankStats.OnFrameListener {
-        override fun onFrame(frameData: FrameData) {
+        override fun onFrame(volatileFrameData: FrameData) {
             ++numFrames
-            if (frameData.isJank) {
-                jankReport.add(frameData)
+            if (volatileFrameData.isJank) {
+                // Store copy of frameData because it will be reused by JankStats before any report
+                // is issued
+                jankReport.add(volatileFrameData.copy())
                 if (jankReport.size >= REPORT_BUFFER_LIMIT) {
                     issueJankReport("Max buffer size reached")
                 }
@@ -54,7 +56,7 @@
         }
     }
 
-    val jankStats = JankStats.createAndTrack(window, executor, listener)
+    val jankStats = JankStats.createAndTrack(window, listener)
 
     private var jankReport = ArrayList()
 
diff --git a/metrics/integration-tests/janktest/src/main/java/androidx/metrics/performance/janktest/SimpleLoggingActivity.kt b/metrics/integration-tests/janktest/src/main/java/androidx/metrics/performance/janktest/SimpleLoggingActivity.kt
index ae5d51f..44ebba1 100644
--- a/metrics/integration-tests/janktest/src/main/java/androidx/metrics/performance/janktest/SimpleLoggingActivity.kt
+++ b/metrics/integration-tests/janktest/src/main/java/androidx/metrics/performance/janktest/SimpleLoggingActivity.kt
@@ -20,24 +20,18 @@
 import android.util.Log
 import android.view.View
 import androidx.appcompat.app.AppCompatActivity
-import androidx.metrics.performance.FrameData
 import androidx.metrics.performance.JankStats
 import androidx.metrics.performance.PerformanceMetricsState
 import java.util.Date
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.asExecutor
 
 class SimpleLoggingActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.simple_layout)
 
-        JankStats.createAndTrack(window, Dispatchers.Default.asExecutor(),
-            object : JankStats.OnFrameListener {
-                override fun onFrame(frameData: FrameData) {
-                    Log.d("MainActivity", frameData.toString())
-                }
-            })
+        JankStats.createAndTrack(window) { volatileFrameData ->
+            Log.d("MainActivity", volatileFrameData.toString())
+        }
 
         findViewById(R.id.button).setOnClickListener {
             val stateHolder = PerformanceMetricsState.getForHierarchy(it).state!!
diff --git a/metrics/metrics-benchmark/build.gradle b/metrics/metrics-benchmark/build.gradle
new file mode 100644
index 0000000..83a3253
--- /dev/null
+++ b/metrics/metrics-benchmark/build.gradle
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("kotlin-android")
+    id("androidx.benchmark")
+}
+
+dependencies {
+    androidTestImplementation("androidx.appcompat:appcompat:1.1.0")
+    androidTestImplementation(project(":benchmark:benchmark-junit4"))
+    androidTestImplementation(project(":metrics:metrics-performance"))
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+}
+
+android {
+    namespace "androidx.metrics.performance.benchmark"
+    defaultConfig {
+        // Enable measuring on an emulator, devices with low battery, and eng builds
+        // We are only concerned with allocations in this benchmark, so runtime performance
+        // problems with these configurations are not relevant
+        testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] =
+                "UNLOCKED,EMULATOR,LOW-BATTERY,ENG-BUILD"
+    }
+}
diff --git a/metrics/metrics-benchmark/src/androidTest/AndroidManifest.xml b/metrics/metrics-benchmark/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..cc97d3c
--- /dev/null
+++ b/metrics/metrics-benchmark/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,27 @@
+
+
+
+        xmlns:android="http://schemas.android.com/apk/res/android">
+    
+            
+            
+
+        
+            android:name="androidx.metrics.performance.benchmark.MainActivity"
+            android:theme="@style/BenchmarkTheme"/>
+    
+
diff --git a/metrics/metrics-benchmark/src/androidTest/java/androidx/metrics/performance/benchmark/JankStatsBenchmark.kt b/metrics/metrics-benchmark/src/androidTest/java/androidx/metrics/performance/benchmark/JankStatsBenchmark.kt
new file mode 100644
index 0000000..61afe7c
--- /dev/null
+++ b/metrics/metrics-benchmark/src/androidTest/java/androidx/metrics/performance/benchmark/JankStatsBenchmark.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.metrics.performance.benchmark
+
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.view.FrameMetrics
+import android.view.Window
+import android.widget.TextView
+import androidx.annotation.RequiresApi
+import androidx.benchmark.junit4.BenchmarkRule
+import androidx.benchmark.junit4.measureRepeated
+import androidx.metrics.performance.FrameData
+import androidx.metrics.performance.JankStats
+import androidx.metrics.performance.JankStatsInternalsForTesting
+import androidx.metrics.performance.PerformanceMetricsState
+import androidx.metrics.performance.benchmark.test.R
+import androidx.test.annotation.UiThreadTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Idea
+ * Want to test per-frame performance. This means need to test what happens when frame data
+ * is sent with and without PerformanceMetricsState. Should also test setting state
+ * (regular and single frame).
+ * Because frame data is received asynchronously, should instrument JankStats and PerformanceMetrics
+ * to allow the key methods to be called from outside (@TestApi)
+ */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class JankStatsBenchmark {
+
+    @get:Rule
+    val benchmarkRule = BenchmarkRule()
+
+    @Suppress("DEPRECATION")
+    @get:Rule
+    val activityRule = androidx.test.rule.ActivityTestRule(MainActivity::class.java)
+
+    lateinit var metricsStateHolder: PerformanceMetricsState.Holder
+    lateinit var jankStats: JankStats
+    lateinit var jankStatsImpl: JankStatsInternalsForTesting
+    lateinit var textview: TextView
+
+    object frameListener : JankStats.OnFrameListener {
+        override fun onFrame(volatileFrameData: FrameData) { }
+    }
+
+    @Before
+    fun setup() {
+        activityRule.runOnUiThread {
+            textview = activityRule.activity.findViewById(R.id.textview)
+            metricsStateHolder = PerformanceMetricsState.getForHierarchy(textview)
+            jankStats = JankStats.createAndTrack(
+                activityRule.activity.window,
+                frameListener
+            )
+            jankStatsImpl = JankStatsInternalsForTesting(jankStats)
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    fun setNewState() {
+        var iteration = 0
+        benchmarkRule.measureRepeated {
+            iteration++
+            metricsStateHolder.state?.addState("Activity$iteration", "activity")
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    fun setStateOverAndOver() {
+        benchmarkRule.measureRepeated {
+            metricsStateHolder.state?.addState("Activity", "activity")
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    fun setAndRemoveState() {
+        benchmarkRule.measureRepeated {
+            // Simply calling removeState() on the public API is not sufficient for benchmarking
+            // allocations, because it will not actually be removed until later, when JankStats
+            // issues data for a frame after the time the state was removed. Thus we call
+            // our testing method here instead to forcibly remove it, which should test the
+            // allocation behavior of the object pool used for states.
+            jankStatsImpl.removeStateNow(metricsStateHolder.state!!, "Activity")
+            metricsStateHolder.state?.addState("Activity", "activity")
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
+    @Test
+    fun getFrameData() {
+        metricsStateHolder.state?.addState("Activity", "activity")
+        benchmarkRule.measureRepeated {
+            jankStatsImpl.getFrameData()
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.N)
+    class Api24TestClass {
+        @Suppress("DEPRECATION")
+        fun frameMetricsTest(
+            activityRule: androidx.test.rule.ActivityTestRule,
+            benchmarkRule: BenchmarkRule,
+            textview: TextView,
+            jankStatsImpl: JankStatsInternalsForTesting
+        ) {
+            var frameMetrics: FrameMetrics? = null
+            val frameMetricsLatch = CountDownLatch(1)
+            val listener = Window.OnFrameMetricsAvailableListener { _, metrics, _ ->
+                frameMetrics = metrics
+                frameMetricsLatch.countDown()
+            }
+            // First have to get a FrameMetrics object, which we cannot create ourselves.
+            // Instead, we will enable FrameMetrics on the window and wait to receive a callback
+            val thread = HandlerThread("FrameMetricsAggregator")
+            thread.start()
+            activityRule.runOnUiThread {
+                activityRule.activity.window.addOnFrameMetricsAvailableListener(
+                    listener,
+                    Handler(thread.looper)
+                )
+                textview.invalidate()
+            }
+            frameMetricsLatch.await(2, TimeUnit.SECONDS)
+            if (frameMetrics != null) {
+                benchmarkRule.measureRepeated {
+                    jankStatsImpl.getFrameData(frameMetrics!!)
+                }
+            }
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+    @Test
+    fun getFrameData24() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            Api24TestClass().frameMetricsTest(activityRule, benchmarkRule, textview, jankStatsImpl)
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
+    @Test
+    fun logFrameData() {
+        metricsStateHolder.state?.addState("Activity", "activity")
+        val frameData = jankStatsImpl.getFrameData()
+        if (frameData != null) {
+            benchmarkRule.measureRepeated {
+                jankStatsImpl.logFrameData(frameData)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/metrics/metrics-benchmark/src/androidTest/java/androidx/metrics/performance/benchmark/MainActivity.kt b/metrics/metrics-benchmark/src/androidTest/java/androidx/metrics/performance/benchmark/MainActivity.kt
new file mode 100644
index 0000000..8d6d624
--- /dev/null
+++ b/metrics/metrics-benchmark/src/androidTest/java/androidx/metrics/performance/benchmark/MainActivity.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.metrics.performance.benchmark
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+
+class MainActivity : AppCompatActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+    }
+}
\ No newline at end of file
diff --git a/metrics/metrics-benchmark/src/androidTest/res/values/styles.xml b/metrics/metrics-benchmark/src/androidTest/res/values/styles.xml
new file mode 100644
index 0000000..cb7668d
--- /dev/null
+++ b/metrics/metrics-benchmark/src/androidTest/res/values/styles.xml
@@ -0,0 +1,27 @@
+
+
+
+
+    
+    
+
diff --git a/metrics/metrics-benchmark/src/main/AndroidManifest.xml b/metrics/metrics-benchmark/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..29b496b
--- /dev/null
+++ b/metrics/metrics-benchmark/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
diff --git a/metrics/metrics-benchmark/src/main/res/layout/activity_main.xml b/metrics/metrics-benchmark/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..6249003
--- /dev/null
+++ b/metrics/metrics-benchmark/src/main/res/layout/activity_main.xml
@@ -0,0 +1,28 @@
+
+
+
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    
+        android:id="@+id/textview"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Hello World!"/>
+
+
\ No newline at end of file
diff --git a/metrics/metrics-performance/api/current.txt b/metrics/metrics-performance/api/current.txt
index 5efa062..74bcb48 100644
--- a/metrics/metrics-performance/api/current.txt
+++ b/metrics/metrics-performance/api/current.txt
@@ -3,6 +3,7 @@
 
   public class FrameData {
     ctor public FrameData(long frameStartNanos, long frameDurationUiNanos, boolean isJank, java.util.List states);
+    method public androidx.metrics.performance.FrameData copy();
     method public final long getFrameDurationUiNanos();
     method public final long getFrameStartNanos();
     method public final java.util.List getStates();
@@ -26,7 +27,7 @@
   }
 
   public final class JankStats {
-    method @UiThread public static androidx.metrics.performance.JankStats createAndTrack(android.view.Window window, java.util.concurrent.Executor executor, androidx.metrics.performance.JankStats.OnFrameListener frameListener);
+    method @UiThread public static androidx.metrics.performance.JankStats createAndTrack(android.view.Window window, androidx.metrics.performance.JankStats.OnFrameListener frameListener);
     method public float getJankHeuristicMultiplier();
     method public boolean isTrackingEnabled();
     method public void setJankHeuristicMultiplier(float);
@@ -37,11 +38,11 @@
   }
 
   public static final class JankStats.Companion {
-    method @UiThread public androidx.metrics.performance.JankStats createAndTrack(android.view.Window window, java.util.concurrent.Executor executor, androidx.metrics.performance.JankStats.OnFrameListener frameListener);
+    method @UiThread public androidx.metrics.performance.JankStats createAndTrack(android.view.Window window, androidx.metrics.performance.JankStats.OnFrameListener frameListener);
   }
 
   public static fun interface JankStats.OnFrameListener {
-    method public void onFrame(androidx.metrics.performance.FrameData frameData);
+    method public void onFrame(androidx.metrics.performance.FrameData volatileFrameData);
   }
 
   public final class PerformanceMetricsState {
diff --git a/metrics/metrics-performance/api/public_plus_experimental_current.txt b/metrics/metrics-performance/api/public_plus_experimental_current.txt
index 5efa062..74bcb48 100644
--- a/metrics/metrics-performance/api/public_plus_experimental_current.txt
+++ b/metrics/metrics-performance/api/public_plus_experimental_current.txt
@@ -3,6 +3,7 @@
 
   public class FrameData {
     ctor public FrameData(long frameStartNanos, long frameDurationUiNanos, boolean isJank, java.util.List states);
+    method public androidx.metrics.performance.FrameData copy();
     method public final long getFrameDurationUiNanos();
     method public final long getFrameStartNanos();
     method public final java.util.List getStates();
@@ -26,7 +27,7 @@
   }
 
   public final class JankStats {
-    method @UiThread public static androidx.metrics.performance.JankStats createAndTrack(android.view.Window window, java.util.concurrent.Executor executor, androidx.metrics.performance.JankStats.OnFrameListener frameListener);
+    method @UiThread public static androidx.metrics.performance.JankStats createAndTrack(android.view.Window window, androidx.metrics.performance.JankStats.OnFrameListener frameListener);
     method public float getJankHeuristicMultiplier();
     method public boolean isTrackingEnabled();
     method public void setJankHeuristicMultiplier(float);
@@ -37,11 +38,11 @@
   }
 
   public static final class JankStats.Companion {
-    method @UiThread public androidx.metrics.performance.JankStats createAndTrack(android.view.Window window, java.util.concurrent.Executor executor, androidx.metrics.performance.JankStats.OnFrameListener frameListener);
+    method @UiThread public androidx.metrics.performance.JankStats createAndTrack(android.view.Window window, androidx.metrics.performance.JankStats.OnFrameListener frameListener);
   }
 
   public static fun interface JankStats.OnFrameListener {
-    method public void onFrame(androidx.metrics.performance.FrameData frameData);
+    method public void onFrame(androidx.metrics.performance.FrameData volatileFrameData);
   }
 
   public final class PerformanceMetricsState {
diff --git a/metrics/metrics-performance/api/restricted_current.txt b/metrics/metrics-performance/api/restricted_current.txt
index 5efa062..74bcb48 100644
--- a/metrics/metrics-performance/api/restricted_current.txt
+++ b/metrics/metrics-performance/api/restricted_current.txt
@@ -3,6 +3,7 @@
 
   public class FrameData {
     ctor public FrameData(long frameStartNanos, long frameDurationUiNanos, boolean isJank, java.util.List states);
+    method public androidx.metrics.performance.FrameData copy();
     method public final long getFrameDurationUiNanos();
     method public final long getFrameStartNanos();
     method public final java.util.List getStates();
@@ -26,7 +27,7 @@
   }
 
   public final class JankStats {
-    method @UiThread public static androidx.metrics.performance.JankStats createAndTrack(android.view.Window window, java.util.concurrent.Executor executor, androidx.metrics.performance.JankStats.OnFrameListener frameListener);
+    method @UiThread public static androidx.metrics.performance.JankStats createAndTrack(android.view.Window window, androidx.metrics.performance.JankStats.OnFrameListener frameListener);
     method public float getJankHeuristicMultiplier();
     method public boolean isTrackingEnabled();
     method public void setJankHeuristicMultiplier(float);
@@ -37,11 +38,11 @@
   }
 
   public static final class JankStats.Companion {
-    method @UiThread public androidx.metrics.performance.JankStats createAndTrack(android.view.Window window, java.util.concurrent.Executor executor, androidx.metrics.performance.JankStats.OnFrameListener frameListener);
+    method @UiThread public androidx.metrics.performance.JankStats createAndTrack(android.view.Window window, androidx.metrics.performance.JankStats.OnFrameListener frameListener);
   }
 
   public static fun interface JankStats.OnFrameListener {
-    method public void onFrame(androidx.metrics.performance.FrameData frameData);
+    method public void onFrame(androidx.metrics.performance.FrameData volatileFrameData);
   }
 
   public final class PerformanceMetricsState {
diff --git a/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt b/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt
index 389b651..bd0f9a2 100644
--- a/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt
+++ b/metrics/metrics-performance/src/androidTest/java/androidx/metrics/performance/test/JankStatsTest.kt
@@ -35,10 +35,8 @@
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 import kotlin.math.max
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.asExecutor
-import org.hamcrest.MatcherAssert.assertThat
 import org.hamcrest.Matchers
+import org.hamcrest.MatcherAssert.assertThat
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNotEquals
@@ -89,11 +87,7 @@
             delayedView = delayedActivity.findViewById(R.id.delayedView)
             latchedListener = LatchedListener()
             latchedListener.latch = CountDownLatch(1)
-            jankStats = JankStats.createAndTrack(
-                delayedActivity.window,
-                Dispatchers.Default.asExecutor(),
-                latchedListener
-            )
+            jankStats = JankStats.createAndTrack(delayedActivity.window, latchedListener)
             metricsState = PerformanceMetricsState.getForHierarchy(delayedView).state!!
         }
     }
@@ -121,13 +115,9 @@
             jsSecond.isTrackingEnabled = !jsSecond.isTrackingEnabled
             jsThird.isTrackingEnabled = !jsThird.isTrackingEnabled
         }
-        delayedActivityRule.scenario.onActivity { _ ->
-            // To repro, must add/remove on the same thread as the one used to call the listeners.
-            // That thread is determined by the Executor. Here's a dirty hack to force the executor
-            // to run on the same thread as the one iterating through the listeners.
-            val executor = Runnable::run
-            jsSecond = JankStats.createAndTrack(delayedActivity.window, executor, secondListener)
-            jsThird = JankStats.createAndTrack(delayedActivity.window, executor, secondListener)
+        delayedActivityRule.scenario.onActivity {
+            jsSecond = JankStats.createAndTrack(delayedActivity.window, secondListener)
+            jsThird = JankStats.createAndTrack(delayedActivity.window, secondListener)
         }
         runDelayTest(frameDelay = 0, NUM_FRAMES, latchedListener)
     }
@@ -143,8 +133,8 @@
 
     @Test
     fun testEquality() {
-        val states1 = listOf(StateInfo("1", "a"))
-        val states2 = listOf(StateInfo("1", "a"), StateInfo("2", "b"))
+        val states1 = listOf(StateInfo("1", "a"))
+        val states2 = listOf(StateInfo("1", "a"), StateInfo("2", "b"))
         val frameDataBase = FrameData(0, 0, true, states1)
         val frameDataBaseCopy = FrameData(0, 0, true, states1)
         val frameDataBaseA = FrameData(0, 0, true, states2)
@@ -210,30 +200,34 @@
     @SdkSuppress(minSdkVersion = JELLY_BEAN)
     @Test
     fun testMultipleListeners() {
+        var secondListenerLatch = CountDownLatch(0)
         val frameDelay = 0
 
         frameInit.initFramePipeline()
 
         var numSecondListenerCalls = 0
         val secondListenerStates = mutableListOf()
-        val secondListener = OnFrameListener {
-            secondListenerStates.addAll(it.states)
+        val secondListener = OnFrameListener { volatileFrameData ->
+            secondListenerStates.addAll(volatileFrameData.states)
             numSecondListenerCalls++
+            if (numSecondListenerCalls >= NUM_FRAMES) {
+                secondListenerLatch.countDown()
+            }
         }
         lateinit var jankStats2: JankStats
         val scenario = delayedActivityRule.scenario
         scenario.onActivity { _ ->
-            jankStats2 = JankStats.createAndTrack(
-                delayedActivity.window, Dispatchers.Default.asExecutor(), secondListener
-            )
+            jankStats2 = JankStats.createAndTrack(delayedActivity.window, secondListener)
         }
         val testState = StateInfo("Testing State", "sampleState")
         metricsState.addSingleFrameState(testState.stateName, testState.state)
 
         // in case earlier frames arrive before our test begins
         secondListenerStates.clear()
+        secondListenerLatch = CountDownLatch(1)
         latchedListener.reset()
         runDelayTest(frameDelay, NUM_FRAMES, latchedListener)
+        secondListenerLatch.await(frameDelay * NUM_FRAMES + 1000L, TimeUnit.MILLISECONDS)
         val jankData: FrameData = latchedListener.jankData[0]
         assertTrue("No calls to second listener", numSecondListenerCalls > 0)
         assertEquals(listOf(testState), jankData.states)
@@ -255,7 +249,7 @@
         poster = Runnable {
             JankStats.createAndTrack(
                 delayedActivity.window,
-                Dispatchers.Default.asExecutor(), secondListener
+                secondListener
             )
             ++numNewListeners
             if (numNewListeners < 100) {
@@ -547,7 +541,7 @@
         }
     }
 
-    inner class LatchedListener : OnFrameListener {
+    inner class LatchedListener : JankStats.OnFrameListener {
         var numJankFrames = 0
         var jankData = mutableListOf()
         var latch: CountDownLatch? = null
@@ -560,17 +554,15 @@
             numFrames = 0
         }
 
-        override fun onFrame(
-            frameData: FrameData
-        ) {
+        override fun onFrame(volatileFrameData: FrameData) {
             if (latch == null) {
                 throw Exception("latch not set in LatchedListener")
             } else {
-                if (frameData.isJank && frameData.frameDurationUiNanos >
+                if (volatileFrameData.isJank && volatileFrameData.frameDurationUiNanos >
                         (MIN_JANK_NS * jankStats.jankHeuristicMultiplier)) {
                     this.numJankFrames++
                 }
-                this.jankData.add(frameData)
+                this.jankData.add(volatileFrameData.copy())
                 numFrames++
                 if (numFrames >= minFrames) {
                     latch!!.countDown()
@@ -578,14 +570,4 @@
             }
         }
     }
-
-    private fun getFrameEndTime(frameData: FrameData): Long {
-        var duration = frameData.frameStartNanos
-        if (frameData is FrameDataApi24) {
-            duration += frameData.frameDurationCpuNanos
-        } else {
-            duration += frameData.frameDurationUiNanos
-        }
-        return duration
-    }
 }
\ No newline at end of file
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/FrameData.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/FrameData.kt
index 3fcb33c..fd464fc 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/FrameData.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/FrameData.kt
@@ -33,11 +33,45 @@
  * @see PerformanceMetricsState.addState
  */
 open class FrameData(
-    val frameStartNanos: Long,
-    val frameDurationUiNanos: Long,
-    val isJank: Boolean,
+    frameStartNanos: Long,
+    frameDurationUiNanos: Long,
+    isJank: Boolean,
     val states: List
 ) {
+    /**
+     * These backing fields are used to enable mutation of an existing FrameData object, to
+     * avoid allocating a new object on every frame for sending out to listeners.
+     */
+    var frameStartNanos = frameStartNanos
+        private set
+    var frameDurationUiNanos = frameDurationUiNanos
+        private set
+    var isJank = isJank
+        private set
+
+    /**
+     * Utility method which makes a copy of the items in this object (including copying the items
+     * in `states` into a new List). This is used internally to create a copy to pass along to
+     * listeners to avoid having a reference to the internally-mutable FrameData object.
+     */
+    open fun copy(): FrameData {
+        return FrameData(frameStartNanos, frameDurationUiNanos, isJank, ArrayList(states))
+    }
+
+    /**
+     * Utility method for updating the internal values in this object. Externally, this object is
+     * immutable. Internally, we need the ability to update the values so that we can reuse
+     * it for a non-allocating listener model, to avoid having to re-allocate a new FrameData
+     * (and its states List). Note that the states object is not being updated here; internal
+     * can already use a Mutable list to update the contents of that list; they do not need to
+     * update this object with a new List, since any usage of FrameData to avoid allocations
+     * should not be creating a new state List anyway.
+     */
+    internal fun update(frameStartNanos: Long, frameDurationUiNanos: Long, isJank: Boolean) {
+        this.frameStartNanos = frameStartNanos
+        this.frameDurationUiNanos = frameDurationUiNanos
+        this.isJank = isJank
+    }
 
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
@@ -99,4 +133,33 @@
     override fun toString(): String {
         return "$stateName: $state"
     }
+
+    /**
+     * This internal componion is used to manage a pool of reusable StateInfo objects.
+     * Rather than creating a new StateInfo object very time, the library requests an object
+     * for the given stateName/state pair. In general, requests will be common using the same
+     * pairs, thus reuse will be high and an object from the pool will be returned. When reuse
+     * is not necessary, a new StateInfo object will be created, added to the pool, and returned.
+     */
+    internal companion object {
+        val pool = mutableMapOf>()
+
+        fun getStateInfo(stateName: String, state: String): StateInfo {
+            synchronized(pool) {
+                var poolItem = pool.get(stateName)
+                var stateInfo = poolItem?.get(state)
+                if (stateInfo != null) return stateInfo
+                else {
+                    stateInfo = StateInfo(stateName, state)
+                    if (poolItem != null) {
+                        poolItem.put(state, stateInfo)
+                    } else {
+                        poolItem = mutableMapOf(state to stateInfo)
+                        pool.put(stateName, poolItem)
+                    }
+                    return stateInfo
+                }
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/FrameDataApi24.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/FrameDataApi24.kt
index ac19adb..6b3717a 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/FrameDataApi24.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/FrameDataApi24.kt
@@ -41,11 +41,29 @@
 open class FrameDataApi24(
     frameStartNanos: Long,
     frameDurationUiNanos: Long,
-    val frameDurationCpuNanos: Long,
+    frameDurationCpuNanos: Long,
     isJank: Boolean,
     states: List
 ) : FrameData(frameStartNanos, frameDurationUiNanos, isJank, states) {
 
+    var frameDurationCpuNanos = frameDurationCpuNanos
+        private set
+
+    override fun copy(): FrameData {
+        return FrameDataApi24(frameStartNanos, frameDurationUiNanos, frameDurationCpuNanos, isJank,
+            ArrayList(states))
+    }
+
+    internal fun update(
+        frameStartNanos: Long,
+        frameDurationUiNanos: Long,
+        frameDurationCpuNanos: Long,
+        isJank: Boolean
+    ) {
+        super.update(frameStartNanos, frameDurationUiNanos, isJank)
+        this.frameDurationCpuNanos = frameDurationCpuNanos
+    }
+
     override fun equals(other: Any?): Boolean {
         return other is FrameDataApi24 &&
             super.equals(other) &&
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/FrameDataApi31.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/FrameDataApi31.kt
index 0efe6c2e..9a9c56a 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/FrameDataApi31.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/FrameDataApi31.kt
@@ -45,12 +45,30 @@
     frameStartNanos: Long,
     frameDurationUiNanos: Long,
     frameDurationCpuNanos: Long,
-    val frameOverrunNanos: Long,
+    frameOverrunNanos: Long,
     isJank: Boolean,
     states: List
-
 ) : FrameDataApi24(frameStartNanos, frameDurationUiNanos, frameDurationCpuNanos, isJank, states) {
 
+    var frameOverrunNanos = frameOverrunNanos
+        private set
+
+    override fun copy(): FrameData {
+        return FrameDataApi31(frameStartNanos, frameDurationUiNanos, frameDurationCpuNanos,
+            frameOverrunNanos, isJank, ArrayList(states))
+    }
+
+    internal fun update(
+        frameStartNanos: Long,
+        frameDurationUiNanos: Long,
+        frameDurationCpuNanos: Long,
+        frameOverrunNanos: Long,
+        isJank: Boolean
+    ) {
+        super.update(frameStartNanos, frameDurationUiNanos, frameDurationCpuNanos, isJank)
+        this.frameOverrunNanos = frameOverrunNanos
+    }
+
     override fun equals(other: Any?): Boolean {
         return other is FrameDataApi31 &&
             super.equals(other) &&
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStats.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStats.kt
index 58939ed..e76e81e0 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStats.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStats.kt
@@ -18,9 +18,9 @@
 import android.os.Build
 import android.view.View
 import android.view.Window
+import androidx.annotation.RestrictTo
 import androidx.annotation.UiThread
 import java.lang.IllegalStateException
-import java.util.concurrent.Executor
 
 /**
  * This class is used to both accumulate and report information about UI "jank" (runtime
@@ -63,7 +63,6 @@
 @Suppress("SingletonConstructor")
 class JankStats private constructor(
     window: Window,
-    private val executor: Executor,
     private val frameListener: OnFrameListener
 ) {
     private val holder: PerformanceMetricsState.Holder
@@ -78,7 +77,8 @@
      * based on the runtime OS version. The JankStats API is basically a think wrapper around
      * the implementations in these classes.
      */
-    private val implementation: JankStatsBaseImpl
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    internal val implementation: JankStatsBaseImpl
 
     init {
         val decorView: View? = window.peekDecorView()
@@ -152,10 +152,8 @@
     /**
      * Called internally (by Impl classes) with the frame data, which is passed onto the client.
      */
-    internal fun logFrameData(frameData: FrameData) {
-        executor.execute {
-            frameListener.onFrame(frameData)
-        }
+    internal fun logFrameData(volatileFrameData: FrameData) {
+        frameListener.onFrame(volatileFrameData)
     }
 
     companion object {
@@ -168,18 +166,21 @@
          */
         @JvmStatic
         @UiThread
-        fun createAndTrack(window: Window, executor: Executor, frameListener: OnFrameListener):
-                JankStats {
-            return JankStats(window, executor, frameListener)
+        @Suppress("ExecutorRegistration")
+        fun createAndTrack(window: Window, frameListener: OnFrameListener): JankStats {
+            return JankStats(window, frameListener)
         }
     }
 
     /**
-     * This listener is called on every frame to supply ongoing jank data to apps.
+     * This interface should be implemented to receive per-frame callbacks with jank data.
      *
-     * [JankStats] requires an implementation of this listener at construction time.
-     * On every frame, the listener is called and receivers can aggregate, store, and upload
-     * the data as appropriate.
+     * Internally, the [FrameData] objected passed to [OnFrameListener.onFrame] is
+     * reused and populated with new data on every frame. This means that listeners
+     * implementing [OnFrameListener] cannot depend on the data received in that
+     * structure over time and should consider the [FrameData] object **obsolete when control
+     * returns from the listener**. Clients wishing to retain data from this call should **copy
+     * the data elsewhere before returning**.
      */
     fun interface OnFrameListener {
 
@@ -187,10 +188,13 @@
          * The implementation of this method will be called on every frame when an
          * OnFrameListener is set on this JankStats object.
          *
-         * @param frameData The data for the frame which just occurred.
+         * The FrameData object **will be modified internally after returning from the listener**;
+         * any data that needs to be retained should be copied before returning.
+         *
+         * @param volatileFrameData The data for the most recent frame.
          */
         fun onFrame(
-            frameData: FrameData
+            volatileFrameData: FrameData
         )
     }
 }
\ No newline at end of file
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi16Impl.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi16Impl.kt
index 88be2de..6c524c4 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi16Impl.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi16Impl.kt
@@ -47,6 +47,15 @@
     // Cache for use during reporting, to supply the FrameData states
     val metricsStateHolder = PerformanceMetricsState.getForHierarchy(view)
 
+    // stateInfo is the backing store for the list of states that are active on any given
+    // frame. It is passed to the JankStats listeners as part of the FrameData structure.
+    // Reusing this mutable version of it enables zero-allocation metrics reporting.
+    val stateInfo = mutableListOf()
+
+    // frameData is reused every time, populated with the latest frame's data before
+    // sending out to listeners. Reuse enables zero-allocation metrics reporting.
+    private val frameData = FrameData(0, 0, false, stateInfo)
+
     /**
      * Each JankStats instance has its own listener for per-frame metric data.
      * But we use a single listener (using OnPreDraw events prior to API 24) to gather
@@ -78,11 +87,11 @@
         uiDuration: Long,
         expectedDuration: Long
     ): FrameData {
-        val frameStates =
-            metricsStateHolder.state?.getIntervalStates(startTime, startTime + uiDuration)
-                ?: emptyList()
+        metricsStateHolder.state?.getIntervalStates(startTime, startTime + uiDuration,
+            stateInfo)
         val isJank = uiDuration > expectedDuration
-        return FrameData(startTime, uiDuration, isJank, frameStates)
+        frameData.update(startTime, uiDuration, isJank)
+        return frameData
     }
 
     private fun View.removeOnPreDrawListenerDelegate(delegate: OnFrameListenerDelegate) {
@@ -267,4 +276,4 @@
             return JankStatsBaseImpl.frameDuration
         }
     }
-}
+}
\ No newline at end of file
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi24Impl.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi24Impl.kt
index f8658c9..8a5d28f 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi24Impl.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi24Impl.kt
@@ -49,6 +49,9 @@
     // overlapped due to jank
     var prevEnd: Long = 0
 
+    // Reuse the same frameData on every frame to avoid allocating per-frame objects
+    private val frameData = FrameDataApi24(0, 0, 0, false, stateInfo)
+
     private val frameMetricsAvailableListenerDelegate: Window.OnFrameMetricsAvailableListener =
         Window.OnFrameMetricsAvailableListener { _, frameMetrics, _ ->
             val startTime = max(getFrameStartTime(frameMetrics), prevEnd)
@@ -74,14 +77,13 @@
             frameMetrics.getMetric(FrameMetrics.DRAW_DURATION) +
             frameMetrics.getMetric(FrameMetrics.SYNC_DURATION)
         prevEnd = startTime + uiDuration
-        val frameStates =
-            metricsStateHolder.state?.getIntervalStates(startTime, prevEnd)
-                ?: emptyList()
+        metricsStateHolder.state?.getIntervalStates(startTime, prevEnd, stateInfo)
         val isJank = uiDuration > expectedDuration
         val cpuDuration = uiDuration +
             frameMetrics.getMetric(FrameMetrics.COMMAND_ISSUE_DURATION) +
             frameMetrics.getMetric(FrameMetrics.SWAP_BUFFERS_DURATION)
-        return FrameDataApi24(startTime, uiDuration, cpuDuration, isJank, frameStates)
+        frameData.update(startTime, uiDuration, cpuDuration, isJank)
+        return frameData
     }
 
     internal open fun getFrameStartTime(frameMetrics: FrameMetrics): Long {
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi31Impl.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi31Impl.kt
index d59a456..1aec5bb 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi31Impl.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsApi31Impl.kt
@@ -28,6 +28,9 @@
     window: Window
 ) : JankStatsApi26Impl(jankStats, view, window) {
 
+    // Reuse the same frameData on every frame to avoid allocating per-frame objects
+    val frameData = FrameDataApi31(0, 0, 0, 0, false, stateInfo)
+
     override fun getFrameData(
         startTime: Long,
         expectedDuration: Long,
@@ -40,16 +43,15 @@
             frameMetrics.getMetric(FrameMetrics.DRAW_DURATION) +
             frameMetrics.getMetric(FrameMetrics.SYNC_DURATION)
         prevEnd = startTime + uiDuration
-        val frameStates =
-            metricsStateHolder.state?.getIntervalStates(startTime, prevEnd)
-                ?: emptyList()
+        metricsStateHolder.state?.getIntervalStates(startTime, prevEnd, stateInfo)
         val isJank = uiDuration > expectedDuration
         val cpuDuration = uiDuration +
             frameMetrics.getMetric(FrameMetrics.COMMAND_ISSUE_DURATION) +
             frameMetrics.getMetric(FrameMetrics.SWAP_BUFFERS_DURATION)
         val overrun = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION) -
             frameMetrics.getMetric(FrameMetrics.DEADLINE)
-        return FrameDataApi31(startTime, uiDuration, cpuDuration, overrun, isJank, frameStates)
+        frameData.update(startTime, uiDuration, cpuDuration, overrun, isJank)
+        return frameData
     }
 
     override fun getExpectedFrameDuration(metrics: FrameMetrics): Long {
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsInternalsForTesting.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsInternalsForTesting.kt
new file mode 100644
index 0000000..2856ef0
--- /dev/null
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/JankStatsInternalsForTesting.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.metrics.performance
+
+import android.os.Build
+import android.view.FrameMetrics
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.annotation.VisibleForTesting
+
+/**
+ * This class exists for calling into otherwise private/internal APIs in JankStats, to allow
+ * for easier, more targeted testing of those internal pieces.
+ */
+@VisibleForTesting
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class JankStatsInternalsForTesting(val jankStats: JankStats) {
+    private val impl = jankStats.implementation
+
+    fun removeStateNow(performanceMetricsState: PerformanceMetricsState, stateName: String) {
+        performanceMetricsState.removeStateNow(stateName)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
+    fun getFrameData(): FrameData? {
+        when (impl) {
+            is JankStatsApi16Impl -> {
+                return impl.getFrameData(0, 0, 0)
+            }
+        }
+        return null
+    }
+
+    @RequiresApi(Build.VERSION_CODES.N)
+    fun getFrameData(frameMetrics: FrameMetrics): FrameData? {
+        when (impl) {
+            is JankStatsApi24Impl -> {
+                return impl.getFrameData(0, 0, frameMetrics)
+            }
+        }
+        return null
+    }
+
+    @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
+    fun logFrameData(frameData: FrameData) {
+        jankStats.logFrameData(frameData)
+    }
+}
\ No newline at end of file
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
index fedae4b..57766f6 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
@@ -61,6 +61,12 @@
     private val statesHolder = mutableListOf()
     private val statesToBeCleared = mutableListOf()
 
+    /**
+     * StateData objects are stored and retrieved from an object pool, to avoid re-allocating
+     * for new state pairs, since it is expected that most states will share names/states
+     */
+    private val stateDataPool = mutableListOf()
+
     private fun addFrameState(
         frameStartTime: Long,
         frameEndTime: Long,
@@ -74,7 +80,7 @@
             val item = activeStates[i]
             if (item.timeRemoved > 0 && item.timeRemoved < frameStartTime) {
                 // remove states that have already been marked for removal
-                activeStates.removeAt(i)
+                returnStateDataToPool(activeStates.removeAt(i))
             } else if (item.timeAdded < frameEndTime) {
                 // Only add unique state. There may be several states added in
                 // a given frame (especially during heavy jank periods). Only the
@@ -142,7 +148,8 @@
         removalTime: Long
     ) {
         synchronized(singleFrameStates) {
-            for (item in states) {
+            for (i in 0 until states.size) {
+                val item = states[i]
                 if (item.state.stateName == stateName && item.timeRemoved < 0) {
                     item.timeRemoved = removalTime
                 }
@@ -177,15 +184,14 @@
         synchronized(singleFrameStates) {
             val nowTime = System.nanoTime()
             markStateForRemoval(stateName, states, nowTime)
+            val stateData = getStateData(
+                nowTime, -1,
+                StateInfo.getStateInfo(stateName, state)
+            )
             states.add(
-                StateData(
-                    nowTime, -1,
-                    StateInfo(stateName, state)
-                )
+                stateData
             )
         }
-        // TODO: consider pooled StateInfo objects that we reuse here instead of creating new
-        // ones every time
     }
 
     /**
@@ -209,7 +215,7 @@
             val nowTime = System.nanoTime()
             markStateForRemoval(stateName, singleFrameStates, nowTime)
             singleFrameStates.add(
-                StateData(
+                getStateData(
                     nowTime, -1,
                     StateInfo(stateName, state)
                 )
@@ -221,6 +227,18 @@
         markStateForRemoval(stateName, states, System.nanoTime())
     }
 
+    internal fun removeStateNow(stateName: String) {
+        synchronized(singleFrameStates) {
+            for (i in 0 until states.size) {
+                val item = states[i]
+                if (item.state.stateName == stateName) {
+                    states.remove(item)
+                    returnStateDataToPool(item)
+                }
+            }
+        }
+    }
+
     /**
      * Internal representation of state information. timeAdded/Removed allows synchronizing states
      * with frame boundaries during the FrameMetrics callback, when we can compare which states
@@ -232,6 +250,31 @@
         var state: StateInfo
     )
 
+    internal fun getStateData(timeAdded: Long, timeRemoved: Long, state: StateInfo): StateData {
+        synchronized(stateDataPool) {
+            if (stateDataPool.isEmpty()) {
+                // This new item will be added to the pool when it is removed, later
+                return StateData(timeAdded, timeRemoved, state)
+            } else {
+                val stateData = stateDataPool.removeAt(0)
+                stateData.timeAdded = timeAdded
+                stateData.timeRemoved = timeRemoved
+                stateData.state = state
+                return stateData
+            }
+        }
+    }
+
+    /**
+     * Once the StateData is done being used, it can be returned to the pool for later reuse,
+     * which happens in getStateData()
+     */
+    internal fun returnStateDataToPool(stateData: StateData) {
+        synchronized(stateDataPool) {
+            stateDataPool.add(stateData)
+        }
+    }
+
     /**
      * Removes information about a specified state.
      *
@@ -258,17 +301,16 @@
      * states added via [addSingleFrameState] are removed, since they have been used
      * exactly once to retrieve the state for this interval.
      */
-    internal fun getIntervalStates(startTime: Long, endTime: Long): List {
-        var frameStates: MutableList
+    internal fun getIntervalStates(
+        startTime: Long,
+        endTime: Long,
+        frameStates: MutableList
+    ) {
         synchronized(singleFrameStates) {
-            frameStates = ArrayList(
-                states.size +
-                    singleFrameStates.size
-            )
+            frameStates.clear()
             addFrameState(startTime, endTime, frameStates, states)
             addFrameState(startTime, endTime, frameStates, singleFrameStates)
         }
-        return frameStates
     }
 
     internal fun cleanupSingleFrameStates() {
@@ -277,7 +319,9 @@
             for (i in singleFrameStates.size - 1 downTo 0) {
                 // SFStates are marked with timeRemoved during processing so we know when
                 // they have logged data and can actually be removed
-                if (singleFrameStates[i].timeRemoved != -1L) singleFrameStates.removeAt(i)
+                if (singleFrameStates[i].timeRemoved != -1L) {
+                    returnStateDataToPool(singleFrameStates.removeAt(i))
+                }
             }
         }
     }
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index f6f7ca1..7153d0f 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -25,7 +25,7 @@
 kotlin.code.style=official
 # Disable docs
 androidx.enableDocumentation=false
-androidx.playground.snapshotBuildId=8757744
+androidx.playground.snapshotBuildId=8818165
 androidx.playground.metalavaBuildId=8670428
 androidx.playground.dokkaBuildId=7472101
 androidx.studio.type=playground
diff --git a/resourceinspection/resourceinspection-annotation/api/1.1.0-beta01.txt b/resourceinspection/resourceinspection-annotation/api/1.1.0-beta01.txt
deleted file mode 100644
index 2962da1..0000000
--- a/resourceinspection/resourceinspection-annotation/api/1.1.0-beta01.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-// Signature format: 4.0
-package androidx.resourceinspection.annotation {
-
-  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.METHOD) public @interface Attribute {
-    method public abstract androidx.resourceinspection.annotation.Attribute.IntMap[] intMapping() default {};
-    method public abstract String value();
-  }
-
-  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({}) public static @interface Attribute.IntMap {
-    method public abstract int mask() default 0;
-    method public abstract String name();
-    method public abstract int value();
-  }
-
-}
-
diff --git a/resourceinspection/resourceinspection-annotation/api/public_plus_experimental_1.1.0-beta01.txt b/resourceinspection/resourceinspection-annotation/api/public_plus_experimental_1.1.0-beta01.txt
deleted file mode 100644
index 2962da1..0000000
--- a/resourceinspection/resourceinspection-annotation/api/public_plus_experimental_1.1.0-beta01.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-// Signature format: 4.0
-package androidx.resourceinspection.annotation {
-
-  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.METHOD) public @interface Attribute {
-    method public abstract androidx.resourceinspection.annotation.Attribute.IntMap[] intMapping() default {};
-    method public abstract String value();
-  }
-
-  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({}) public static @interface Attribute.IntMap {
-    method public abstract int mask() default 0;
-    method public abstract String name();
-    method public abstract int value();
-  }
-
-}
-
diff --git a/resourceinspection/resourceinspection-annotation/api/restricted_1.1.0-beta01.txt b/resourceinspection/resourceinspection-annotation/api/restricted_1.1.0-beta01.txt
deleted file mode 100644
index 5be0bd5..0000000
--- a/resourceinspection/resourceinspection-annotation/api/restricted_1.1.0-beta01.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-// Signature format: 4.0
-package androidx.resourceinspection.annotation {
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.TYPE) public @interface AppCompatShadowedAttributes {
-  }
-
-  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target(java.lang.annotation.ElementType.METHOD) public @interface Attribute {
-    method public abstract androidx.resourceinspection.annotation.Attribute.IntMap[] intMapping() default {};
-    method public abstract String value();
-  }
-
-  @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({}) public static @interface Attribute.IntMap {
-    method public abstract int mask() default 0;
-    method public abstract String name();
-    method public abstract int value();
-  }
-
-}
-
diff --git a/settings.gradle b/settings.gradle
index 06481eb..4caef1e 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -714,6 +714,7 @@
 includeProject(":mediarouter:mediarouter", [BuildType.MAIN, BuildType.MEDIA])
 includeProject(":mediarouter:mediarouter-testing", [BuildType.MAIN, BuildType.MEDIA])
 includeProject(":metrics:metrics-performance", [BuildType.MAIN])
+includeProject(":metrics:metrics-benchmark", [BuildType.MAIN])
 includeProject(":metrics:metrics-integration-tests", "metrics/integration-tests", [BuildType.MAIN])
 includeProject(":metrics:metrics-integration-tests:janktest", "metrics/integration-tests/janktest", [BuildType.MAIN])
 includeProject(":navigation:navigation-benchmark", [BuildType.MAIN, BuildType.FLAN])
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Button.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Button.java
index 14cc781..81f4392 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Button.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/Button.java
@@ -169,8 +169,8 @@
         /**
          * Sets the size for the {@link Button}. Strongly recommended values are {@link
          * ButtonDefaults#DEFAULT_SIZE}, {@link ButtonDefaults#LARGE_SIZE} and {@link
-         * ButtonDefaults#EXTRA_LARGE_SIZE}. If not set, {@link
-         * ButtonDefaults#DEFAULT_SIZE} will be used.
+         * ButtonDefaults#EXTRA_LARGE_SIZE}. If not set, {@link ButtonDefaults#DEFAULT_SIZE} will be
+         * used.
          */
         @NonNull
         public Builder setSize(@NonNull DpProp size) {
@@ -181,8 +181,8 @@
         /**
          * Sets the size for the {@link Button}. Strongly recommended values are {@link
          * ButtonDefaults#DEFAULT_SIZE}, {@link ButtonDefaults#LARGE_SIZE} and {@link
-         * ButtonDefaults#EXTRA_LARGE_SIZE}. If not set, {@link
-         * ButtonDefaults#DEFAULT_SIZE} will be used.
+         * ButtonDefaults#EXTRA_LARGE_SIZE}. If not set, {@link ButtonDefaults#DEFAULT_SIZE} will be
+         * used.
          */
         @NonNull
         public Builder setSize(@Dimension(unit = DP) float size) {
@@ -242,12 +242,11 @@
 
         /**
          * Sets the content of this Button to be the given text with the default font for the set
-         * size (for the {@link ButtonDefaults#DEFAULT_SIZE}, {@link
-         * ButtonDefaults#LARGE_SIZE} and {@link ButtonDefaults#EXTRA_LARGE_SIZE} is
-         * {@link Typography#TYPOGRAPHY_TITLE2}, {@link Typography#TYPOGRAPHY_TITLE1} and {@link
-         * Typography#TYPOGRAPHY_DISPLAY3} respectively). Any previously added content will be
-         * overridden. Text should contain no more than 3 characters, otherwise it will overflow
-         * from the edges.
+         * size (for the {@link ButtonDefaults#DEFAULT_SIZE}, {@link ButtonDefaults#LARGE_SIZE} and
+         * {@link ButtonDefaults#EXTRA_LARGE_SIZE} is {@link Typography#TYPOGRAPHY_TITLE2}, {@link
+         * Typography#TYPOGRAPHY_TITLE1} and {@link Typography#TYPOGRAPHY_DISPLAY3} respectively).
+         * Any previously added content will be overridden. Text should contain no more than 3
+         * characters, otherwise it will overflow from the edges.
          */
         @NonNull
         public Builder setTextContent(@NonNull String text) {
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/LayoutDefaults.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/LayoutDefaults.java
index b540215..df5d7d0 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/LayoutDefaults.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/LayoutDefaults.java
@@ -45,13 +45,25 @@
      * The default percentage for the top margin for primary chip in the {@link PrimaryLayout} on
      * square devices.
      */
-    static final float PRIMARY_LAYOUT_MARGIN_TOP_SQUARE_PERCENT = 15.6f / 100;
+    static final float PRIMARY_LAYOUT_MARGIN_TOP_SQUARE_PERCENT = 13.3f / 100;
+
+    /**
+     * The default spacer above primary label in {@link PrimaryLayout} to make space for Tile icon
+     * on round devices.
+     */
+    static final DpProp PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_ROUND_DP = dp(0);
+
+    /**
+     * The default spacer above primary label in {@link PrimaryLayout} to make space for Tile icon
+     * on square devices.
+     */
+    static final DpProp PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_SQUARE_DP = dp(4);
 
     /**
      * The default percentage for the horizontal margin for primary chip in the {@link
      * PrimaryLayout}.
      */
-    static final float PRIMARY_LAYOUT_MARGIN_HORIZONTAL_ROUND_PERCENT = 5.2f / 100;
+    static final float PRIMARY_LAYOUT_MARGIN_HORIZONTAL_ROUND_PERCENT = 6.3f / 100;
 
     /**
      * The default percentage for the horizontal margin for primary chip in the {@link
@@ -63,7 +75,7 @@
      * The padding for the primary chip in {@link PrimaryLayout} so it doesn't bleed off screen if
      * text is too big.
      */
-    static final float PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_ROUND_DP = 32;
+    static final float PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_ROUND_DP = 30;
 
     /**
      * The padding for the primary chip in {@link PrimaryLayout} so it doesn't bleed off screen if
@@ -113,7 +125,7 @@
     static final DpProp MULTI_BUTTON_1_SIZE = ButtonDefaults.EXTRA_LARGE_SIZE;
 
     /** The default width for vertical spacer between buttons in the {@link MultiButtonLayout}. */
-    static final DpProp MULTI_BUTTON_SPACER_WIDTH = dp(8);
+    static final DpProp MULTI_BUTTON_SPACER_WIDTH = dp(6);
 
     /**
      * The default height for horizontal spacer between buttons in the {@link MultiButtonLayout}.
diff --git a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/PrimaryLayout.java b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/PrimaryLayout.java
index 228e91a..ce84643 100644
--- a/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/PrimaryLayout.java
+++ b/wear/tiles/tiles-material/src/main/java/androidx/wear/tiles/material/layouts/PrimaryLayout.java
@@ -35,6 +35,8 @@
 import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_HORIZONTAL_SQUARE_PERCENT;
 import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_TOP_ROUND_PERCENT;
 import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_MARGIN_TOP_SQUARE_PERCENT;
+import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_ROUND_DP;
+import static androidx.wear.tiles.material.layouts.LayoutDefaults.PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_SQUARE_DP;
 
 import android.annotation.SuppressLint;
 
@@ -128,6 +130,13 @@
      */
     static final int CONTENT_PRESENT = 0x8;
 
+    /** Position of the primary label in inner column if exists. */
+    static final int PRIMARY_LABEL_POSITION = 1;
+    /** Position of the content in inner column element when primary label is present. */
+    static final int CONTENT_POSITION = 3;
+    /** Position of the content in inner column element when there primary label is not present. */
+    static final int CONTENT_ONLY_POSITION = 0;
+
     /** @hide */
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     @Retention(RetentionPolicy.SOURCE)
@@ -250,6 +259,8 @@
                             .setHorizontalAlignment(LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER);
 
             if (mPrimaryLabelText != null) {
+                innerContentBuilder.addContent(
+                        new Spacer.Builder().setHeight(getPrimaryLabelTopSpacerHeight()).build());
                 innerContentBuilder.addContent(mPrimaryLabelText);
                 innerContentBuilder.addContent(
                         new Spacer.Builder().setHeight(mVerticalSpacerHeight).build());
@@ -372,6 +383,14 @@
                     ? PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_ROUND_DP
                     : PRIMARY_LAYOUT_CHIP_HORIZONTAL_PADDING_SQUARE_DP;
         }
+
+        /** Returns the spacer height to be placed above primary label to accommodate Tile icon. */
+        @NonNull
+        private DpProp getPrimaryLabelTopSpacerHeight() {
+            return isRoundDevice(mDeviceParameters)
+                    ? PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_ROUND_DP
+                    : PRIMARY_LAYOUT_PRIMARY_LABEL_SPACER_HEIGHT_SQUARE_DP;
+        }
     }
 
     /** Get the primary label content from this layout. */
@@ -380,8 +399,7 @@
         if (!areElementsPresent(PRIMARY_LABEL_PRESENT)) {
             return null;
         }
-        // By tag we know that primary label exists. It will always be at position 0.
-        return mInnerColumn.get(0);
+        return mInnerColumn.get(PRIMARY_LABEL_POSITION);
     }
 
     /** Get the secondary label content from this layout. */
@@ -400,9 +418,9 @@
         if (!areElementsPresent(CONTENT_PRESENT)) {
             return null;
         }
-        // By tag we know that content exists. It will be at position 0 if there is no primary
-        // label, or at position 2 (primary label, spacer - content) otherwise.
-        int contentPosition = areElementsPresent(PRIMARY_LABEL_PRESENT) ? 2 : 0;
+        int contentPosition =
+                areElementsPresent(PRIMARY_LABEL_PRESENT)
+                        ? CONTENT_POSITION : CONTENT_ONLY_POSITION;
         return ((Box) mInnerColumn.get(contentPosition)).getContents().get(0);
     }
 
@@ -421,13 +439,13 @@
     @SuppressLint("ResourceType")
     @Dimension(unit = DP)
     public float getVerticalSpacerHeight() {
-        // We don't need special cases for primary or secondary label - if primary label is present,
-        // then the first spacer is at the position 1 and we can get height from it. However, if the
-        // primary label is not present, the spacer will be between content and secondary label (if
-        // there is secondary label) so its position is again 1.
         if (areElementsPresent(PRIMARY_LABEL_PRESENT)
                 || areElementsPresent(SECONDARY_LABEL_PRESENT)) {
-            LayoutElement element = mInnerColumn.get(1);
+            LayoutElement element =
+                    mInnerColumn.get(
+                            (areElementsPresent(PRIMARY_LABEL_PRESENT)
+                                    ? PRIMARY_LABEL_POSITION : CONTENT_ONLY_POSITION) + 1
+                    );
             if (element instanceof Spacer) {
                 SpacerDimension height = ((Spacer) element).getHeight();
                 if (height instanceof DpProp) {
diff --git a/wear/tiles/tiles-material/src/test/java/androidx/wear/tiles/material/layouts/PrimaryLayoutTest.java b/wear/tiles/tiles-material/src/test/java/androidx/wear/tiles/material/layouts/PrimaryLayoutTest.java
index e6d34e5..1f749e6 100644
--- a/wear/tiles/tiles-material/src/test/java/androidx/wear/tiles/material/layouts/PrimaryLayoutTest.java
+++ b/wear/tiles/tiles-material/src/test/java/androidx/wear/tiles/material/layouts/PrimaryLayoutTest.java
@@ -153,13 +153,13 @@
     public void testWrongTag() {
         Box box =
                 new Box.Builder()
-                    .setModifiers(
-                        new Modifiers.Builder()
-                            .setMetadata(
-                                new ElementMetadata.Builder()
-                                    .setTagData("test".getBytes(UTF_8))
-                                    .build())
-                                .build())
+                        .setModifiers(
+                                new Modifiers.Builder()
+                                        .setMetadata(
+                                                new ElementMetadata.Builder()
+                                                        .setTagData("test".getBytes(UTF_8))
+                                                        .build())
+                                        .build())
                         .build();
 
         assertThat(PrimaryLayout.fromLayoutElement(box)).isNull();
@@ -169,15 +169,15 @@
     public void testWrongLengthTag() {
         Box box =
                 new Box.Builder()
-                    .setModifiers(
-                        new Modifiers.Builder()
-                            .setMetadata(
-                                new ElementMetadata.Builder()
-                                    .setTagData(
-                                        PrimaryLayout.METADATA_TAG_PREFIX
-                                            .getBytes(UTF_8))
-                                    .build())
-                            .build())
+                        .setModifiers(
+                                new Modifiers.Builder()
+                                        .setMetadata(
+                                                new ElementMetadata.Builder()
+                                                        .setTagData(
+                                                                PrimaryLayout.METADATA_TAG_PREFIX
+                                                                        .getBytes(UTF_8))
+                                                        .build())
+                                        .build())
                         .build();
 
         assertThat(PrimaryLayout.fromLayoutElement(box)).isNull();
@@ -227,18 +227,22 @@
             assertThat(actualLayout.getContent()).isNull();
         } else {
             assertThat(actualLayout.getContent().toLayoutElementProto())
-                .isEqualTo(expectedContent.toLayoutElementProto());
+                    .isEqualTo(expectedContent.toLayoutElementProto());
             expectedMetadata[PrimaryLayout.FLAG_INDEX] =
-                (byte) (expectedMetadata[PrimaryLayout.FLAG_INDEX] | PrimaryLayout.CONTENT_PRESENT);
+                    (byte)
+                            (expectedMetadata[PrimaryLayout.FLAG_INDEX]
+                                    | PrimaryLayout.CONTENT_PRESENT);
         }
 
         if (expectedPrimaryChip == null) {
             assertThat(actualLayout.getPrimaryChipContent()).isNull();
         } else {
             assertThat(actualLayout.getPrimaryChipContent().toLayoutElementProto())
-                .isEqualTo(expectedPrimaryChip.toLayoutElementProto());
+                    .isEqualTo(expectedPrimaryChip.toLayoutElementProto());
             expectedMetadata[PrimaryLayout.FLAG_INDEX] =
-                (byte) (expectedMetadata[PrimaryLayout.FLAG_INDEX] | PrimaryLayout.CHIP_PRESENT);
+                    (byte)
+                            (expectedMetadata[PrimaryLayout.FLAG_INDEX]
+                                    | PrimaryLayout.CHIP_PRESENT);
         }
 
         assertThat(actualLayout.getVerticalSpacerHeight()).isEqualTo(height);
@@ -247,22 +251,22 @@
             assertThat(actualLayout.getPrimaryLabelTextContent()).isNull();
         } else {
             assertThat(actualLayout.getPrimaryLabelTextContent().toLayoutElementProto())
-                .isEqualTo(expectedPrimaryLabel.toLayoutElementProto());
+                    .isEqualTo(expectedPrimaryLabel.toLayoutElementProto());
             expectedMetadata[PrimaryLayout.FLAG_INDEX] =
-                (byte)
-                    (expectedMetadata[PrimaryLayout.FLAG_INDEX]
-                        | PrimaryLayout.PRIMARY_LABEL_PRESENT);
+                    (byte)
+                            (expectedMetadata[PrimaryLayout.FLAG_INDEX]
+                                    | PrimaryLayout.PRIMARY_LABEL_PRESENT);
         }
 
         if (expectedSecondaryLabel == null) {
             assertThat(actualLayout.getSecondaryLabelTextContent()).isNull();
         } else {
             assertThat(actualLayout.getSecondaryLabelTextContent().toLayoutElementProto())
-                .isEqualTo(expectedSecondaryLabel.toLayoutElementProto());
+                    .isEqualTo(expectedSecondaryLabel.toLayoutElementProto());
             expectedMetadata[PrimaryLayout.FLAG_INDEX] =
-                (byte)
-                    (expectedMetadata[PrimaryLayout.FLAG_INDEX]
-                        | PrimaryLayout.SECONDARY_LABEL_PRESENT);
+                    (byte)
+                            (expectedMetadata[PrimaryLayout.FLAG_INDEX]
+                                    | PrimaryLayout.SECONDARY_LABEL_PRESENT);
         }
 
         assertThat(actualLayout.getMetadataTag()).isEqualTo(expectedMetadata);
diff --git a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java
index 34f094d..88efeb2 100644
--- a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java
+++ b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java
@@ -64,6 +64,7 @@
 import com.google.common.truth.Truth;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
@@ -1101,6 +1102,7 @@
         assertThat(output).isEqualTo(Color.WHITE);
     }
 
+    @Ignore // b/238635208
     @Test
     public void placeholderIcon() {
         mComplicationRenderer.setComplicationData(
@@ -1112,6 +1114,7 @@
         assertThat(mComplicationRenderer.mIsPlaceholderIcon).isTrue();
     }
 
+    @Ignore // b/238635208
     @Test
     public void placeholderSmallImage() {
         mComplicationRenderer.setComplicationData(
@@ -1123,6 +1126,7 @@
         assertThat(mComplicationRenderer.mIsPlaceholderSmallImage).isTrue();
     }
 
+    @Ignore // b/238635208
     @Test
     public void placeholderPhotoImage() {
         mComplicationRenderer.setComplicationData(
diff --git a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
index be20dbb..0d1a4e8 100644
--- a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
+++ b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
@@ -29,6 +29,7 @@
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assert.fail
 import org.junit.Assert.assertThrows
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -900,6 +901,7 @@
         assertThat(digestHash1).isNotEqualTo(digestHash2)
     }
 
+    @Ignore // b/238635208
     @Test
     public fun digestHashSensitiveToIconChanges() {
         val colorStyleSetting1 = ListUserStyleSetting(
diff --git a/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java b/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java
index 7648800..c31650f 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java
@@ -20,13 +20,12 @@
 import android.view.KeyEvent;
 import android.view.animation.Animation;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
 import androidx.annotation.UiThread;
 import androidx.wear.utils.ActivityAnimationUtil;
 
-import org.jetbrains.annotations.NotNull;
-
 /**
  * Controller that handles the back button click for dismiss the frame layout
  *
@@ -49,7 +48,7 @@
                         && dismiss());
     }
 
-    void disable(@NotNull DismissibleFrameLayout layout) {
+    void disable(@NonNull DismissibleFrameLayout layout) {
         setOnDismissListener(null);
         layout.setOnKeyListener(null);
         // setting this to false will also ensure that this view is not focusable in touch mode
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java
index f657cc2..23d44ca 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatTest.java
@@ -167,4 +167,22 @@
             Assert.assertEquals("androidx.webkit.test", headerValue);
         }
     }
+
+    @Test
+    public void testEnterpriseAuthenticationAppLinkPolicyEnabled() throws Throwable {
+        WebkitUtils.checkFeature(WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY);
+
+        assertTrue(WebSettingsCompat.getEnterpriseAuthenticationAppLinkPolicyEnabled(
+                mWebViewOnUiThread.getSettings()));
+
+        WebSettingsCompat.setEnterpriseAuthenticationAppLinkPolicyEnabled(
+                mWebViewOnUiThread.getSettings(), false);
+        assertFalse(WebSettingsCompat.getEnterpriseAuthenticationAppLinkPolicyEnabled(
+                mWebViewOnUiThread.getSettings()));
+
+        WebSettingsCompat.setEnterpriseAuthenticationAppLinkPolicyEnabled(
+                mWebViewOnUiThread.getSettings(), true);
+        assertTrue(WebSettingsCompat.getEnterpriseAuthenticationAppLinkPolicyEnabled(
+                mWebViewOnUiThread.getSettings()));
+    }
 }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
index 477c312e..9e97fc8 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
@@ -724,6 +724,65 @@
         }
     }
 
+    /**
+     * Sets whether EnterpriseAuthenticationAppLinkPolicy if set by admin is allowed to have any
+     * effect on WebView.
+     * 

+ * EnterpriseAuthenticationAppLinkPolicy in WebView allows admins to specify authentication + * urls. When WebView is redirected to authentication url, and an app on the device has + * registered as the default handler for the url, that app is launched. + *

+ * EnterpriseAuthenticationAppLinkPolicy is enabled by default. + * + *

+ * This method should only be called if + * {@link WebViewFeature#isFeatureSupported(String)} + * returns true for {@link WebViewFeature#ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY}. + * + * @param enabled Whether EnterpriseAuthenticationAppLinkPolicy should be enabled. + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY) + @RequiresFeature(name = WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY, + enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported") + public static void setEnterpriseAuthenticationAppLinkPolicyEnabled( + @NonNull WebSettings settings, + boolean enabled) { + ApiFeature.NoFramework feature = + WebViewFeatureInternal.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY; + if (feature.isSupportedByWebView()) { + getAdapter(settings).setEnterpriseAuthenticationAppLinkPolicyEnabled(enabled); + } else { + throw WebViewFeatureInternal.getUnsupportedOperationException(); + } + } + + /** + * Gets whether EnterpriseAuthenticationAppLinkPolicy is allowed to have any effect on WebView. + * + *

+ * This method should only be called if + * {@link WebViewFeature#isFeatureSupported(String)} + * returns true for {@link WebViewFeature#ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY}. + * + * @return {@code true} if EnterpriseAuthenticationAppLinkPolicy is enabled and {@code false} + * otherwise. + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY) + @RequiresFeature(name = WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY, + enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported") + public static boolean getEnterpriseAuthenticationAppLinkPolicyEnabled( + @NonNull WebSettings settings) { + ApiFeature.NoFramework feature = + WebViewFeatureInternal.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY; + if (feature.isSupportedByWebView()) { + return getAdapter(settings).getEnterpriseAuthenticationAppLinkPolicyEnabled(); + } else { + throw WebViewFeatureInternal.getUnsupportedOperationException(); + } + } + private static WebSettingsAdapter getAdapter(WebSettings settings) { return WebViewGlueCommunicator.getCompatConverter().convertSettings(settings); }

diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
index 5de5148..8d50e6c 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -98,6 +98,7 @@
             GET_VARIATIONS_HEADER,
             ALGORITHMIC_DARKENING,
             REQUESTED_WITH_HEADER_CONTROL,
+            ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY,
     })
     @Retention(RetentionPolicy.SOURCE)
     @Target({ElementType.PARAMETER, ElementType.METHOD})
@@ -492,6 +493,18 @@
     public static final String REQUESTED_WITH_HEADER_CONTROL = "REQUESTED_WITH_HEADER_CONTROL";
 
     /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link WebSettingsCompat#setEnterpriseAuthenticationAppLinkPolicyEnabled(WebSettings, boolean)}and
+     * {@link WebSettingsCompat#getEnterpriseAuthenticationAppLinkPolicyEnabled(WebSettings)}.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static final String ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY =
+            "ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY";
+
+    /**
      * Return whether a feature is supported at run-time. On devices running Android version {@link
      * android.os.Build.VERSION_CODES#LOLLIPOP} and higher, this will check whether a feature is
      * supported, depending on the combination of the desired feature, the Android version of
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java
index 963fb99..8d66c85 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java
@@ -140,6 +140,22 @@
 
     /**
      * Adapter method for
+     * {@link androidx.webkit.WebSettingsCompat#setEnterpriseAuthenticationAppLinkPolicyEnabled}.
+     */
+    public void setEnterpriseAuthenticationAppLinkPolicyEnabled(boolean enabled) {
+        mBoundaryInterface.setEnterpriseAuthenticationAppLinkPolicyEnabled(enabled);
+    }
+
+    /**
+     * Adapter method for
+     * {@link androidx.webkit.WebSettingsCompat#getEnterpriseAuthenticationAppLinkPolicyEnabled}.
+     */
+    public boolean getEnterpriseAuthenticationAppLinkPolicyEnabled() {
+        return mBoundaryInterface.getEnterpriseAuthenticationAppLinkPolicyEnabled();
+    }
+
+    /**
+     * Adapter method for
      * {@link androidx.webkit.WebSettingsCompat#getRequestedWithHeaderMode(android.webkit.WebSettings)}
      */
     public int getRequestedWithHeaderMode() {
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
index fc179e8..aa106c7 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
@@ -473,6 +473,15 @@
             new ApiFeature.NoFramework(WebViewFeature.REQUESTED_WITH_HEADER_CONTROL,
                     Features.REQUESTED_WITH_HEADER_CONTROL);
 
+
+    /**
+     * This feature covers
+     * {@link androidx.webkit.WebSettingsCompat#setEnterpriseAuthenticationAppLinkPolicyEnabled(WebSettings, boolean)} and
+     * {@link androidx.webkit.WebSettingsCompat#getEnterpriseAuthenticationAppLinkPolicyEnabled(WebSettings)}.
+     */
+    public static final ApiFeature.NoFramework ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY =
+            new ApiFeature.NoFramework(WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY,
+                    Features.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY);
     // --- Add new feature constants above this line ---
 
     private WebViewFeatureInternal() {
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
index c90bcc8..ea3ad1f 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkUpdateTest.kt
@@ -22,6 +22,7 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
 import androidx.work.WorkInfo.State
 import androidx.work.WorkManager.UpdateResult.APPLIED_FOR_NEXT_RUN
 import androidx.work.WorkManager.UpdateResult.APPLIED_IMMEDIATELY
@@ -35,6 +36,8 @@
 import androidx.work.impl.testutils.TestConstraintTracker
 import androidx.work.impl.utils.taskexecutor.TaskExecutor
 import androidx.work.impl.utils.taskexecutor.WorkManagerTaskExecutor
+import androidx.work.impl.workers.ARGUMENT_CLASS_NAME
+import androidx.work.impl.workers.ConstraintTrackingWorker
 import androidx.work.worker.LatchWorker
 import androidx.work.worker.RetryWorker
 import androidx.work.worker.TestWorker
@@ -413,8 +416,10 @@
         val spec = workManager.workDatabase.workSpecDao().getWorkSpec(request.stringId)!!
         val delta = spec.calculateNextRunTime() - System.currentTimeMillis()
         assertThat(delta).isGreaterThan(0)
-        workManager.workDatabase.workSpecDao().setLastEnqueuedTime(request.stringId,
-            spec.lastEnqueueTime - delta)
+        workManager.workDatabase.workSpecDao().setLastEnqueuedTime(
+            request.stringId,
+            spec.lastEnqueueTime - delta
+        )
         val updated = OneTimeWorkRequest.Builder(TestWorker::class.java).setId(request.id)
             .setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.DAYS)
             .build()
@@ -431,8 +436,10 @@
             .setInitialDelay(10, TimeUnit.MINUTES).build()
         val enqueueTime = System.currentTimeMillis()
         workManager.enqueue(request).result.get()
-        workManager.workDatabase.workSpecDao().setLastEnqueuedTime(request.stringId,
-            enqueueTime - TimeUnit.MINUTES.toMillis(5))
+        workManager.workDatabase.workSpecDao().setLastEnqueuedTime(
+            request.stringId,
+            enqueueTime - TimeUnit.MINUTES.toMillis(5)
+        )
         val updated = OneTimeWorkRequest.Builder(TestWorker::class.java)
             .setInitialDelay(20, TimeUnit.MINUTES)
             .setId(request.id)
@@ -446,6 +453,24 @@
         assertThat(delta).isLessThan(TimeUnit.MINUTES.toMillis(16))
     }
 
+    @Test
+    @MediumTest
+    @SdkSuppress(minSdkVersion = 23, maxSdkVersion = 25)
+    fun testUpdatePeriodicWorker_preservesConstraintTrackingWorker() {
+        val originRequest = OneTimeWorkRequest.Builder(TestWorker::class.java)
+            .setInitialDelay(10, TimeUnit.HOURS).build()
+        workManager.enqueue(originRequest).result.get()
+        val updateRequest = OneTimeWorkRequest.Builder(RetryWorker::class.java)
+            .setId(originRequest.id).setInitialDelay(10, TimeUnit.HOURS)
+            .setConstraints(Constraints(requiresBatteryNotLow = true))
+            .build()
+        workManager.updateWork(updateRequest).get()
+        val workSpec = db.workSpecDao().getWorkSpec(originRequest.stringId)!!
+        assertThat(workSpec.workerClassName).isEqualTo(ConstraintTrackingWorker::class.java.name)
+        assertThat(workSpec.input.getString(ARGUMENT_CLASS_NAME))
+            .isEqualTo(RetryWorker::class.java.name)
+    }
+
     @After
     fun tearDown() {
         workManager.cancelAllWork()
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkerUpdater.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkerUpdater.kt
index 236e52c..b4435ea 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkerUpdater.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkerUpdater.kt
@@ -23,6 +23,7 @@
 import androidx.work.WorkRequest
 import androidx.work.impl.model.WorkSpec
 import androidx.work.impl.utils.futures.SettableFuture
+import androidx.work.impl.utils.wrapInConstraintTrackingWorkerIfNeeded
 import com.google.common.util.concurrent.ListenableFuture
 
 private fun updateWorkImpl(
@@ -57,7 +58,8 @@
             runAttemptCount = oldWorkSpec.runAttemptCount,
             lastEnqueueTime = oldWorkSpec.lastEnqueueTime,
         )
-        workSpecDao.updateWorkSpec(updatedSpec)
+
+        workSpecDao.updateWorkSpec(wrapInConstraintTrackingWorkerIfNeeded(schedulers, updatedSpec))
         workTagDao.deleteByWorkSpecId(workSpecId)
         workTagDao.insertTags(workRequest)
         if (!isEnqueued) {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java
index cbd798f..5408bb6 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueRunnable.java
@@ -25,24 +25,20 @@
 import static androidx.work.WorkInfo.State.FAILED;
 import static androidx.work.WorkInfo.State.RUNNING;
 import static androidx.work.WorkInfo.State.SUCCEEDED;
-import static androidx.work.impl.workers.ConstraintTrackingWorkerKt.ARGUMENT_CLASS_NAME;
+import static androidx.work.impl.utils.EnqueueUtilsKt.wrapInConstraintTrackingWorkerIfNeeded;
 
 import android.content.Context;
-import android.os.Build;
 import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
-import androidx.work.Constraints;
-import androidx.work.Data;
 import androidx.work.ExistingWorkPolicy;
 import androidx.work.Logger;
 import androidx.work.Operation;
 import androidx.work.WorkInfo;
 import androidx.work.WorkRequest;
 import androidx.work.impl.OperationImpl;
-import androidx.work.impl.Scheduler;
 import androidx.work.impl.Schedulers;
 import androidx.work.impl.WorkContinuationImpl;
 import androidx.work.impl.WorkDatabase;
@@ -53,7 +49,6 @@
 import androidx.work.impl.model.WorkName;
 import androidx.work.impl.model.WorkSpec;
 import androidx.work.impl.model.WorkSpecDao;
-import androidx.work.impl.workers.ConstraintTrackingWorker;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -306,20 +301,17 @@
                 workSpec.lastEnqueueTime = currentTimeMillis;
             }
 
-            if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL
-                    && Build.VERSION.SDK_INT <= 25) {
-                tryDelegateConstrainedWorkSpec(workSpec);
-            } else if (Build.VERSION.SDK_INT <= WorkManagerImpl.MAX_PRE_JOB_SCHEDULER_API_LEVEL
-                    && usesScheduler(workManagerImpl, Schedulers.GCM_SCHEDULER)) {
-                tryDelegateConstrainedWorkSpec(workSpec);
-            }
-
             // If we have one WorkSpec with an enqueued state, then we need to schedule.
             if (workSpec.state == ENQUEUED) {
                 needsScheduling = true;
             }
 
-            workDatabase.workSpecDao().insertWorkSpec(workSpec);
+            workDatabase.workSpecDao().insertWorkSpec(
+                    wrapInConstraintTrackingWorkerIfNeeded(
+                            workManagerImpl.getSchedulers(),
+                            workSpec
+                    )
+            );
 
             if (hasPrerequisite) {
                 for (String prerequisiteId : prerequisiteIds) {
@@ -335,50 +327,4 @@
         }
         return needsScheduling;
     }
-
-    private static void tryDelegateConstrainedWorkSpec(WorkSpec workSpec) {
-        // requiresBatteryNotLow and requiresStorageNotLow require API 26 for JobScheduler.
-        // Delegate to ConstraintTrackingWorker between API 23-25.
-        Constraints constraints = workSpec.constraints;
-        String workerClassName = workSpec.workerClassName;
-        // Check if the Worker is a ConstraintTrackingWorker already. Otherwise we could end up
-        // wrapping a ConstraintTrackingWorker with another and build a taller stack.
-        // This usually happens when a developer accidentally enqueues() a named WorkRequest
-        // with an ExistingWorkPolicy.KEEP and subsequent inserts no-op (while the state of the
-        // Worker is not ENQUEUED or RUNNING i.e. the Worker probably just got done & the app is
-        // holding on to a reference of WorkSpec which got updated). We end up reusing the
-        // WorkSpec, and get a ConstraintTrackingWorker (instead of the original Worker class).
-        boolean isConstraintTrackingWorker =
-                workerClassName.equals(ConstraintTrackingWorker.class.getName());
-        if (!isConstraintTrackingWorker
-                && (constraints.requiresBatteryNotLow() || constraints.requiresStorageNotLow())) {
-            Data.Builder builder = new Data.Builder();
-            // Copy all arguments
-            builder.putAll(workSpec.input)
-                    .putString(ARGUMENT_CLASS_NAME, workerClassName);
-            workSpec.workerClassName = ConstraintTrackingWorker.class.getName();
-            workSpec.input = builder.build();
-        }
-    }
-
-    /**
-     * @param className The fully qualified class name of the {@link Scheduler}
-     * @return {@code true} if the {@link Scheduler} class is being used by WorkManager.
-     */
-    private static boolean usesScheduler(
-            @NonNull WorkManagerImpl workManager,
-            @NonNull String className) {
-
-        try {
-            Class klass = Class.forName(className);
-            for (Scheduler scheduler : workManager.getSchedulers()) {
-                if (klass.isAssignableFrom(scheduler.getClass())) {
-                    return true;
-                }
-            }
-            return false;
-        } catch (ClassNotFoundException ignore) {
-            return false;
-        }
-    }
 }
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueUtils.kt b/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueUtils.kt
new file mode 100644
index 0000000..a6cc806
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/EnqueueUtils.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.impl.utils
+
+import android.os.Build
+import androidx.work.Data
+import androidx.work.impl.Scheduler
+import androidx.work.impl.Schedulers
+import androidx.work.impl.WorkManagerImpl.MAX_PRE_JOB_SCHEDULER_API_LEVEL
+import androidx.work.impl.WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL
+import androidx.work.impl.model.WorkSpec
+import androidx.work.impl.workers.ARGUMENT_CLASS_NAME
+import androidx.work.impl.workers.ConstraintTrackingWorker
+
+internal fun tryDelegateConstrainedWorkSpec(workSpec: WorkSpec): WorkSpec {
+    // requiresBatteryNotLow and requiresStorageNotLow require API 26 for JobScheduler.
+    // Delegate to ConstraintTrackingWorker between API 23-25.
+    val constraints = workSpec.constraints
+    val workerClassName = workSpec.workerClassName
+    // Check if the Worker is a ConstraintTrackingWorker already. Otherwise we could end up
+    // wrapping a ConstraintTrackingWorker with another and build a taller stack.
+    // This usually happens when a developer accidentally enqueues() a named WorkRequest
+    // with an ExistingWorkPolicy.KEEP and subsequent inserts no-op (while the state of the
+    // Worker is not ENQUEUED or RUNNING i.e. the Worker probably just got done & the app is
+    // holding on to a reference of WorkSpec which got updated). We end up reusing the
+    // WorkSpec, and get a ConstraintTrackingWorker (instead of the original Worker class).
+    val isConstraintTrackingWorker = workerClassName == ConstraintTrackingWorker::class.java.name
+    if (!isConstraintTrackingWorker &&
+        (constraints.requiresBatteryNotLow() || constraints.requiresStorageNotLow())
+    ) {
+        val newInputData = Data.Builder().putAll(workSpec.input)
+            .putString(ARGUMENT_CLASS_NAME, workerClassName)
+            .build()
+        return workSpec.copy(
+            input = newInputData,
+            workerClassName = ConstraintTrackingWorker::class.java.name
+        )
+    }
+    return workSpec
+}
+
+internal fun wrapInConstraintTrackingWorkerIfNeeded(
+    schedulers: List,
+    workSpec: WorkSpec,
+): WorkSpec {
+    return when {
+        Build.VERSION.SDK_INT in MIN_JOB_SCHEDULER_API_LEVEL..25 ->
+            tryDelegateConstrainedWorkSpec(workSpec)
+        Build.VERSION.SDK_INT <= MAX_PRE_JOB_SCHEDULER_API_LEVEL &&
+            usesScheduler(schedulers, Schedulers.GCM_SCHEDULER) ->
+            tryDelegateConstrainedWorkSpec(workSpec)
+        else -> workSpec
+    }
+}
+
+private fun usesScheduler(
+    schedulers: List,
+    className: String
+): Boolean {
+    return try {
+        val klass = Class.forName(className)
+        return schedulers.any { scheduler -> klass.isAssignableFrom(scheduler.javaClass) }
+    } catch (ignore: ClassNotFoundException) {
+        false
+    }
+}