Merge "Assert on executor being idle in WriteAheadLoggingTest" into androidx-main
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
index 1b5b012..55e0c22 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
@@ -144,6 +144,13 @@
  */
 const val KMP_ENABLE_LINUX = "androidx.kmp.linux.enabled"
 
+/**
+ * If true, include all native targets when building KMP.
+ * Replaces KMP_ENABLE_MAC and KMP_ENABLE_LINUX in collections, and will eventually be
+ * consolidated into the AndroidX plugin.
+ */
+const val KMP_ENABLE_NATIVE = "androidx.kmp.native.enabled"
+
 val ALL_ANDROIDX_PROPERTIES = setOf(
     ALL_WARNINGS_AS_ERRORS,
     ALTERNATIVE_PROJECT_URL,
@@ -171,7 +178,8 @@
     KMP_GITHUB_BUILD,
     KMP_ENABLE_MAC,
     KMP_ENABLE_JS,
-    KMP_ENABLE_LINUX
+    KMP_ENABLE_LINUX,
+    KMP_ENABLE_NATIVE
 )
 
 /**
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
index 602b86b..a35c78f 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ProcessingCaptureSessionTest.kt
@@ -27,31 +27,40 @@
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.TotalCaptureResult
 import android.media.ImageReader
+import android.media.ImageWriter
 import android.os.Build
 import android.os.Handler
 import android.os.Looper
+import android.os.SystemClock
 import android.util.Size
 import android.view.Surface
 import androidx.camera.camera2.Camera2Config
 import androidx.camera.camera2.internal.compat.CameraManagerCompat
 import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks
+import androidx.camera.camera2.internal.util.RequestProcessorRequest
 import androidx.camera.camera2.interop.CaptureRequestOptions
+import androidx.camera.core.CameraInfo
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
 import androidx.camera.core.impl.CameraCaptureCallback
+import androidx.camera.core.impl.CameraCaptureFailure
 import androidx.camera.core.impl.CameraCaptureResult
 import androidx.camera.core.impl.CaptureConfig
 import androidx.camera.core.impl.Config
 import androidx.camera.core.impl.DeferrableSurface
 import androidx.camera.core.impl.ImmediateSurface
+import androidx.camera.core.impl.OptionsBundle
+import androidx.camera.core.impl.OutputSurface
+import androidx.camera.core.impl.RequestProcessor
 import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.SessionProcessor
+import androidx.camera.core.impl.SessionProcessorSurface
 import androidx.camera.core.impl.TagBundle
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.CameraDeviceHolder
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
-import androidx.camera.testing.fakes.FakeSessionProcessor
 import androidx.concurrent.futures.await
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
@@ -70,6 +79,7 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withTimeout
+import kotlinx.coroutines.withTimeoutOrNull
 import org.junit.After
 import org.junit.Assume.assumeTrue
 import org.junit.Before
@@ -78,6 +88,7 @@
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
+const val FAKE_CAPTURE_SEQUENCE_ID = 1
 const val JPEG_ORIENTATION_VALUE = 90
 const val JPEG_QUALITY_VALUE: Byte = 50
 
@@ -94,9 +105,9 @@
 @SdkSuppress(minSdkVersion = 23)
 class ProcessingCaptureSessionTest(
     private var lensFacing: Int,
-    // The pair specifies (Output image format to Input image format). SessionProcessor will
-    // create the surface even if input format is the same as output format. But if the
-    // output format is null, it means no conversion and original surface is used directly.
+    // The pair specifies (Origin image format to Transformed format), SessionProcessor will
+    // create the surface even if transformed format is the same as origin format but if the
+    // transformed format is null, it means no conversion and original surface is used directly.
     private var previewFormatConvert: Pair<Int, Int?>,
     private var captureFormatConvert: Pair<Int, Int?>,
 ) {
@@ -133,7 +144,7 @@
     private lateinit var cameraDeviceHolder: CameraDeviceHolder
     private lateinit var captureSessionRepository: CaptureSessionRepository
     private lateinit var captureSessionOpenerBuilder: SynchronizedCaptureSessionOpener.Builder
-    private lateinit var sessionProcessor: FakeSessionProcessor
+    private lateinit var sessionProcessor: CustomSessionProcessor
     private lateinit var executor: Executor
     private lateinit var handler: Handler
     private lateinit var sessionConfigParameters: SessionConfigParameters
@@ -151,10 +162,7 @@
 
         handler = Handler(Looper.getMainLooper())
         executor = CameraXExecutors.newHandlerExecutor(handler)
-        sessionProcessor = FakeSessionProcessor(
-            inputFormatPreview = previewFormatConvert.second,
-            inputFormatCapture = captureFormatConvert.second
-        )
+        sessionProcessor = CustomSessionProcessor()
 
         val cameraId = CameraUtil.getCameraIdWithLensFacing(lensFacing)!!
         camera2CameraInfo = Camera2CameraInfoImpl(cameraId, cameraManagerCompat)
@@ -815,7 +823,7 @@
             )
             captureOutputDeferrableSurface = ImmediateSurface(
                 captureImageReader.surface, Size(640, 480),
-                captureFormat
+                JPEG
             )
             captureOutputDeferrableSurface.setContainerClass(ImageCapture::class.java)
             captureOutputDeferrableSurface.terminationFuture.addListener(
@@ -925,4 +933,297 @@
             closeOutputSurfaces()
         }
     }
+
+    inner class CustomSessionProcessor : SessionProcessor {
+        private lateinit var previewProcessorSurface: DeferrableSurface
+        private lateinit var captureProcessorSurface: DeferrableSurface
+        private var intermediaPreviewImageReader: ImageReader? = null
+        private var intermediaCaptureImageReader: ImageReader? = null
+        private var intermediaPreviewImageWriter: ImageWriter? = null
+        private var intermediaCaptureImageWriter: ImageWriter? = null
+
+        private val previewOutputConfigId = 1
+        private val captureOutputConfigId = 2
+
+        private var requestProcessor: RequestProcessor? = null
+
+        // Values of these Deferred are the timestamp to complete.
+        private val initSessionCalled = CompletableDeferred<Long>()
+        private val deInitSessionCalled = CompletableDeferred<Long>()
+        private val onCaptureSessionStartCalled = CompletableDeferred<Long>()
+        private val onCaptureSessionEndCalled = CompletableDeferred<Long>()
+        private val startRepeatingCalled = CompletableDeferred<Long>()
+        private val startCaptureCalled = CompletableDeferred<Long>()
+        private val setParametersCalled = CompletableDeferred<Config>()
+        private var latestParameters: Config = OptionsBundle.emptyBundle()
+        private var blockRunAfterInitSession: () -> Unit = {}
+
+        fun releaseSurfaces() {
+            intermediaPreviewImageReader?.close()
+            intermediaCaptureImageReader?.close()
+        }
+
+        fun runAfterInitSession(block: () -> Unit) {
+            blockRunAfterInitSession = block
+        }
+
+        override fun initSession(
+            cameraInfo: CameraInfo,
+            previewSurfaceConfig: OutputSurface,
+            imageCaptureSurfaceConfig: OutputSurface,
+            imageAnalysisSurfaceConfig: OutputSurface?
+        ): SessionConfig {
+            initSessionCalled.complete(SystemClock.elapsedRealtimeNanos())
+            val handler = Handler(Looper.getMainLooper())
+
+            var sessionBuilder = SessionConfig.Builder()
+
+            // Preview
+            lateinit var previewTransformedSurface: Surface
+            if (previewFormatConvert.second == null) { // no conversion, use origin surface.
+                previewTransformedSurface = previewSurfaceConfig.surface
+            } else {
+                intermediaPreviewImageReader = ImageReader.newInstance(
+                    640, 480,
+                    previewFormatConvert.second!!, 2
+                )
+                previewTransformedSurface = intermediaPreviewImageReader!!.surface
+
+                intermediaPreviewImageWriter = ImageWriter.newInstance(
+                    previewSurfaceConfig.surface, 2
+                )
+
+                intermediaPreviewImageReader!!.setOnImageAvailableListener(
+                    {
+                        it.acquireNextImage().use {
+                            val imageDequeued = intermediaPreviewImageWriter!!.dequeueInputImage()
+                            intermediaPreviewImageWriter!!.queueInputImage(imageDequeued)
+                        }
+                    },
+                    handler
+                )
+            }
+            previewProcessorSurface =
+                SessionProcessorSurface(previewTransformedSurface, previewOutputConfigId)
+            previewProcessorSurface.terminationFuture.addListener(
+                {
+                    intermediaPreviewImageReader?.close()
+                    intermediaPreviewImageWriter?.close()
+                },
+                CameraXExecutors.directExecutor()
+            )
+            sessionBuilder.addSurface(previewProcessorSurface)
+
+            // Capture
+            lateinit var captureTransformedSurface: Surface
+            if (captureFormatConvert.second == null) { // no conversion, use origin surface.
+                captureTransformedSurface = imageCaptureSurfaceConfig.surface
+            } else {
+                intermediaCaptureImageReader = ImageReader.newInstance(
+                    640, 480,
+                    captureFormatConvert.second!!, 2
+                )
+                captureTransformedSurface = intermediaCaptureImageReader!!.surface
+
+                intermediaCaptureImageWriter = ImageWriter.newInstance(
+                    imageCaptureSurfaceConfig.surface, 2
+                )
+
+                intermediaCaptureImageReader!!.setOnImageAvailableListener(
+                    {
+                        it.acquireNextImage().use {
+                            val imageDequeued = intermediaCaptureImageWriter!!.dequeueInputImage()
+                            intermediaCaptureImageWriter!!.queueInputImage(imageDequeued)
+                        }
+                    },
+                    handler
+                )
+            }
+            captureProcessorSurface =
+                SessionProcessorSurface(captureTransformedSurface, captureOutputConfigId)
+
+            captureProcessorSurface.terminationFuture.addListener(
+                {
+                    intermediaCaptureImageReader?.close()
+                    intermediaCaptureImageWriter?.close()
+                },
+                CameraXExecutors.directExecutor()
+            )
+            sessionBuilder.addSurface(captureProcessorSurface)
+
+            sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG)
+            val sessionConfig = sessionBuilder.build()
+            blockRunAfterInitSession()
+            return sessionConfig
+        }
+
+        override fun deInitSession() {
+            deInitSessionCalled.complete(SystemClock.elapsedRealtimeNanos())
+            previewProcessorSurface.close()
+            captureProcessorSurface.close()
+        }
+
+        override fun setParameters(config: Config) {
+            setParametersCalled.complete(config)
+            latestParameters = config
+        }
+
+        override fun onCaptureSessionStart(_requestProcessor: RequestProcessor) {
+            onCaptureSessionStartCalled.complete(SystemClock.elapsedRealtimeNanos())
+            requestProcessor = _requestProcessor
+        }
+
+        override fun onCaptureSessionEnd() {
+            onCaptureSessionEndCalled.complete(SystemClock.elapsedRealtimeNanos())
+        }
+
+        fun getLatestParameters(): Config {
+            return latestParameters
+        }
+
+        override fun startRepeating(callback: SessionProcessor.CaptureCallback): Int {
+            startRepeatingCalled.complete(SystemClock.elapsedRealtimeNanos())
+            val builder = RequestProcessorRequest.Builder().apply {
+                addTargetOutputConfigId(previewOutputConfigId)
+                setParameters(latestParameters)
+                setTemplateId(CameraDevice.TEMPLATE_PREVIEW)
+            }
+
+            requestProcessor!!.setRepeating(
+                builder.build(),
+                object : RequestProcessor.Callback {
+                    override fun onCaptureStarted(
+                        request: RequestProcessor.Request,
+                        frameNumber: Long,
+                        timestamp: Long
+                    ) {}
+
+                    override fun onCaptureProgressed(
+                        request: RequestProcessor.Request,
+                        captureResult: CameraCaptureResult
+                    ) {}
+
+                    override fun onCaptureCompleted(
+                        request: RequestProcessor.Request,
+                        captureResult: CameraCaptureResult
+                    ) {
+                        callback.onCaptureSequenceCompleted(1)
+                    }
+
+                    override fun onCaptureFailed(
+                        request: RequestProcessor.Request,
+                        captureFailure: CameraCaptureFailure
+                    ) {}
+
+                    override fun onCaptureBufferLost(
+                        request: RequestProcessor.Request,
+                        frameNumber: Long,
+                        outputConfigId: Int
+                    ) {}
+
+                    override fun onCaptureSequenceCompleted(
+                        sequenceId: Int,
+                        frameNumber: Long
+                    ) {}
+
+                    override fun onCaptureSequenceAborted(sequenceId: Int) {
+                    }
+                }
+            )
+            return FAKE_CAPTURE_SEQUENCE_ID
+        }
+
+        override fun stopRepeating() {
+        }
+
+        override fun startCapture(callback: SessionProcessor.CaptureCallback): Int {
+            startCaptureCalled.complete(SystemClock.elapsedRealtimeNanos())
+            val request = RequestProcessorRequest.Builder().apply {
+                addTargetOutputConfigId(captureOutputConfigId)
+                setTemplateId(CameraDevice.TEMPLATE_STILL_CAPTURE)
+            }.build()
+
+            requestProcessor!!.submit(
+                request,
+                object : RequestProcessor.Callback {
+                    override fun onCaptureCompleted(
+                        request: RequestProcessor.Request,
+                        captureResult: CameraCaptureResult
+                    ) {
+                        callback.onCaptureSequenceCompleted(1)
+                    }
+
+                    override fun onCaptureStarted(
+                        request: RequestProcessor.Request,
+                        frameNumber: Long,
+                        timestamp: Long
+                    ) {}
+
+                    override fun onCaptureProgressed(
+                        request: RequestProcessor.Request,
+                        captureResult: CameraCaptureResult
+                    ) {}
+
+                    override fun onCaptureFailed(
+                        request: RequestProcessor.Request,
+                        captureFailure: CameraCaptureFailure
+                    ) {
+                        callback.onCaptureFailed(1)
+                    }
+
+                    override fun onCaptureBufferLost(
+                        request: RequestProcessor.Request,
+                        frameNumber: Long,
+                        outputConfigId: Int
+                    ) {}
+
+                    override fun onCaptureSequenceCompleted(sequenceId: Int, frameNumber: Long) {}
+
+                    override fun onCaptureSequenceAborted(sequenceId: Int) {}
+                }
+            )
+            return FAKE_CAPTURE_SEQUENCE_ID
+        }
+
+        override fun abortCapture(captureSequenceId: Int) {
+        }
+
+        suspend fun assertInitSessionInvoked(): Long {
+            return initSessionCalled.awaitWithTimeout(3000)
+        }
+
+        suspend fun wasInitSessionInvoked(): Boolean {
+            val result = withTimeoutOrNull(3000) { initSessionCalled.await() }
+            return result != null
+        }
+
+        suspend fun assertDeInitSessionInvoked(): Long {
+            return deInitSessionCalled.awaitWithTimeout(3000)
+        }
+
+        suspend fun assertOnCaptureSessionStartInvoked(): Long {
+            return onCaptureSessionStartCalled.awaitWithTimeout(3000)
+        }
+
+        suspend fun wasOnCaptureSessionStartInvoked(): Boolean {
+            val result = withTimeoutOrNull(3000) { onCaptureSessionStartCalled.await() }
+            return result != null
+        }
+
+        suspend fun assertOnCaptureEndInvoked(): Long {
+            return onCaptureSessionEndCalled.awaitWithTimeout(3000)
+        }
+
+        suspend fun assertStartRepeatingInvoked(): Long {
+            return startRepeatingCalled.awaitWithTimeout(3000)
+        }
+
+        suspend fun assertStartCaptureInvoked(): Long {
+            return startCaptureCalled.awaitWithTimeout(3000)
+        }
+
+        suspend fun assertSetParametersInvoked(): Config {
+            return setParametersCalled.awaitWithTimeout(3000)
+        }
+    }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
index b3dd1ee..203c4dc 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ProcessingCaptureSession.java
@@ -17,7 +17,6 @@
 package androidx.camera.camera2.internal;
 
 import android.hardware.camera2.CameraDevice;
-import android.hardware.camera2.CaptureRequest;
 import android.util.Size;
 
 import androidx.annotation.NonNull;
@@ -344,25 +343,9 @@
                 break;
             case ON_CAPTURE_SESSION_STARTED:
                 mIsExecutingStillCaptureRequest = true;
-                CaptureRequestOptions.Builder builder =
-                        CaptureRequestOptions.Builder.from(
-                                captureConfig.getImplementationOptions());
-
-                if (captureConfig.getImplementationOptions().containsOption(
-                        CaptureConfig.OPTION_ROTATION)) {
-                    builder.setCaptureRequestOption(CaptureRequest.JPEG_ORIENTATION,
-                            captureConfig.getImplementationOptions().retrieveOption(
-                                    CaptureConfig.OPTION_ROTATION));
-                }
-
-                if (captureConfig.getImplementationOptions().containsOption(
-                        CaptureConfig.OPTION_JPEG_QUALITY)) {
-                    builder.setCaptureRequestOption(CaptureRequest.JPEG_QUALITY,
-                            captureConfig.getImplementationOptions().retrieveOption(
-                                    CaptureConfig.OPTION_JPEG_QUALITY).byteValue());
-                }
-
-                mStillCaptureOptions = builder.build();
+                mStillCaptureOptions =
+                        CaptureRequestOptions.Builder.from(captureConfig.getImplementationOptions())
+                                .build();
                 updateParameters(mSessionOptions, mStillCaptureOptions);
                 mSessionProcessor.startCapture(new SessionProcessor.CaptureCallback() {
                     @Override
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
index 50d851e..f831878 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
@@ -166,7 +166,7 @@
         // Act.
         pipeline.executeCapture(
             listOf(singleRequest),
-            ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+            FLASH_MODE_OFF,
         )
 
         // Assert.
diff --git a/camera/camera-core/lint-baseline.xml b/camera/camera-core/lint-baseline.xml
index aaff45e..4aba0fb 100644
--- a/camera/camera-core/lint-baseline.xml
+++ b/camera/camera-core/lint-baseline.xml
@@ -128,6 +128,116 @@
         errorLine1="    @Override"
         errorLine2="    ^">
         <location
+            file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+            line="53"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="BanSynchronizedMethods"
+        message="Use of synchronized methods is not recommended"
+        errorLine1="    @Override"
+        errorLine2="    ^">
+        <location
+            file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+            line="76"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="BanSynchronizedMethods"
+        message="Use of synchronized methods is not recommended"
+        errorLine1="    @Override"
+        errorLine2="    ^">
+        <location
+            file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+            line="103"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="BanSynchronizedMethods"
+        message="Use of synchronized methods is not recommended"
+        errorLine1="    @Override"
+        errorLine2="    ^">
+        <location
+            file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+            line="108"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="BanSynchronizedMethods"
+        message="Use of synchronized methods is not recommended"
+        errorLine1="    @Override"
+        errorLine2="    ^">
+        <location
+            file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+            line="113"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="BanSynchronizedMethods"
+        message="Use of synchronized methods is not recommended"
+        errorLine1="    @Override"
+        errorLine2="    ^">
+        <location
+            file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+            line="118"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="BanSynchronizedMethods"
+        message="Use of synchronized methods is not recommended"
+        errorLine1="    @Override"
+        errorLine2="    ^">
+        <location
+            file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+            line="123"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="BanSynchronizedMethods"
+        message="Use of synchronized methods is not recommended"
+        errorLine1="    @Nullable"
+        errorLine2="    ^">
+        <location
+            file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+            line="128"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="BanSynchronizedMethods"
+        message="Use of synchronized methods is not recommended"
+        errorLine1="    @Override"
+        errorLine2="    ^">
+        <location
+            file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+            line="134"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="BanSynchronizedMethods"
+        message="Use of synchronized methods is not recommended"
+        errorLine1="    @Override"
+        errorLine2="    ^">
+        <location
+            file="src/main/java/androidx/camera/core/AndroidImageReaderProxy.java"
+            line="146"
+            column="5"/>
+    </issue>
+
+    <issue
+        id="BanSynchronizedMethods"
+        message="Use of synchronized methods is not recommended"
+        errorLine1="    @Override"
+        errorLine2="    ^">
+        <location
             file="src/main/java/androidx/camera/core/ForwardingImageProxy.java"
             line="65"
             column="5"/>
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ModifiableImageReaderProxyTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/ModifiableImageReaderProxyTest.kt
deleted file mode 100644
index be57eaf..0000000
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ModifiableImageReaderProxyTest.kt
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core
-
-import android.graphics.ImageFormat
-import android.graphics.Matrix
-import android.media.ImageReader
-import android.media.ImageWriter
-import android.os.Handler
-import androidx.camera.core.impl.ImageReaderProxy
-import androidx.camera.core.impl.MutableTagBundle
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Mockito
-import org.mockito.Mockito.spy
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 23) // This test uses ImageWriter which is supported from api 23.
-class ModifiableImageReaderProxyTest {
-    private lateinit var imageReader: ImageReader
-    private lateinit var imageReaderProxy: ModifiableImageReaderProxy
-    private var imageWriter: ImageWriter? = null
-
-    @Before
-    fun setUp() {
-        imageReader = spy(ImageReader.newInstance(640, 480, ImageFormat.YUV_420_888, 2))
-        imageReaderProxy = ModifiableImageReaderProxy(imageReader)
-    }
-
-    @After
-    fun tearDown() {
-        imageReaderProxy.close()
-        imageWriter?.close()
-    }
-
-    @Test
-    fun canModifyImageTagBundle_acquireNext() {
-        generateImage(imageReader)
-
-        val tagBundle = MutableTagBundle.create()
-        imageReaderProxy.setImageTagBundle(tagBundle)
-        val imageProxy = imageReaderProxy.acquireNextImage()
-        assertThat(imageProxy!!.imageInfo.tagBundle).isEqualTo(tagBundle)
-    }
-
-    @Test
-    fun canModifyImageTagBundle_acquireLatest() {
-        generateImage(imageReader)
-
-        val tagBundle = MutableTagBundle.create()
-        imageReaderProxy.setImageTagBundle(tagBundle)
-        val imageProxy = imageReaderProxy.acquireLatestImage()
-        assertThat(imageProxy!!.imageInfo.tagBundle).isEqualTo(tagBundle)
-        imageProxy.close()
-    }
-
-    @Test
-    fun canModifyImageTimestamp_acquireNext() {
-        generateImage(imageReader)
-
-        imageReaderProxy.setImageTimeStamp(1000)
-        val imageProxy = imageReaderProxy.acquireNextImage()
-        assertThat(imageProxy!!.imageInfo.timestamp).isEqualTo(1000)
-        imageProxy.close()
-    }
-
-    @Test
-    fun canModifyImageTimestamp_acquireLatest() {
-        generateImage(imageReader)
-
-        imageReaderProxy.setImageTimeStamp(1000)
-        val imageProxy = imageReaderProxy.acquireLatestImage()
-        assertThat(imageProxy!!.imageInfo.timestamp).isEqualTo(1000)
-        imageProxy.close()
-    }
-
-    @Test
-    fun canModifyImageRotationDegrees_acquireNext() {
-        generateImage(imageReader)
-
-        imageReaderProxy.setImageRotationDegrees(90)
-        val imageProxy = imageReaderProxy.acquireNextImage()
-        assertThat(imageProxy!!.imageInfo.rotationDegrees).isEqualTo(90)
-        imageProxy.close()
-    }
-
-    @Test
-    fun canModifyImageRotationDegress_acquireLatest() {
-        generateImage(imageReader)
-
-        imageReaderProxy.setImageRotationDegrees(90)
-        val imageProxy = imageReaderProxy.acquireLatestImage()
-        assertThat(imageProxy!!.imageInfo.rotationDegrees).isEqualTo(90)
-        imageProxy.close()
-    }
-
-    @Test
-    fun canModifyImageMatrix_acquireNext() {
-        generateImage(imageReader)
-
-        val matrix = Matrix()
-        imageReaderProxy.setImageSensorToBufferTransformaMatrix(matrix)
-        val imageProxy = imageReaderProxy.acquireNextImage()
-        assertThat(imageProxy!!.imageInfo.sensorToBufferTransformMatrix).isSameInstanceAs(matrix)
-        imageProxy.close()
-    }
-
-    @Test
-    fun canModifyImageMatrix_acquireLatest() {
-        generateImage(imageReader)
-
-        val matrix = Matrix()
-        imageReaderProxy.setImageSensorToBufferTransformaMatrix(matrix)
-        val imageProxy = imageReaderProxy.acquireLatestImage()
-        assertThat(imageProxy!!.imageInfo.sensorToBufferTransformMatrix).isSameInstanceAs(matrix)
-        imageProxy.close()
-    }
-
-    private fun generateImage(imageReader: ImageReader) {
-        imageWriter = ImageWriter.newInstance(imageReader.surface, 2)
-        val image = imageWriter!!.dequeueInputImage()
-        imageWriter!!.queueInputImage(image)
-    }
-
-    @Test
-    fun parametersMatchesInnerImageReader() {
-        assertThat(imageReaderProxy.width).isEqualTo(640)
-        assertThat(imageReaderProxy.height).isEqualTo(480)
-        assertThat(imageReaderProxy.imageFormat).isEqualTo(ImageFormat.YUV_420_888)
-        assertThat(imageReaderProxy.maxImages).isEqualTo(2)
-        assertThat(imageReaderProxy.surface).isEqualTo(imageReader.surface)
-    }
-
-    @Test
-    fun setOnImageAvailableListener_innerReaderIsInvoked() {
-        val listener = Mockito.mock(
-            ImageReaderProxy.OnImageAvailableListener::class.java
-        )
-
-        imageReaderProxy.setOnImageAvailableListener(
-            listener,
-            CameraXExecutors.directExecutor()
-        )
-
-        val transformedListenerCaptor = ArgumentCaptor.forClass(
-            ImageReader.OnImageAvailableListener::class.java
-        )
-        val handlerCaptor = ArgumentCaptor.forClass(
-            Handler::class.java
-        )
-        Mockito.verify(imageReader, Mockito.times(1))
-            .setOnImageAvailableListener(
-                transformedListenerCaptor.capture(), handlerCaptor.capture()
-            )
-
-        transformedListenerCaptor.value.onImageAvailable(imageReader)
-        Mockito.verify(listener, Mockito.times(1)).onImageAvailable(imageReaderProxy)
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/AndroidImageReaderProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/AndroidImageReaderProxy.java
index 92efe7d..ea2a97e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/AndroidImageReaderProxy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/AndroidImageReaderProxy.java
@@ -36,10 +36,9 @@
  * ImageReader}.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-class AndroidImageReaderProxy implements ImageReaderProxy {
-    @GuardedBy("mLock")
+final class AndroidImageReaderProxy implements ImageReaderProxy {
+    @GuardedBy("this")
     private final ImageReader mImageReader;
-    private final Object mLock = new Object();
 
     /**
      * Creates a new instance which wraps the given image reader.
@@ -53,52 +52,48 @@
 
     @Override
     @Nullable
-    public ImageProxy acquireLatestImage() {
-        synchronized (mLock) {
-            Image image;
-            try {
-                image = mImageReader.acquireLatestImage();
-            } catch (RuntimeException e) {
+    public synchronized ImageProxy acquireLatestImage() {
+        Image image;
+        try {
+            image = mImageReader.acquireLatestImage();
+        } catch (RuntimeException e) {
             /* In API level 23 or below,  it will throw "java.lang.RuntimeException:
                ImageReaderContext is not initialized" when ImageReader is closed. To make the
                behavior consistent as newer API levels,  we make it return null Image instead.*/
-                if (isImageReaderContextNotInitializedException(e)) {
-                    image = null;
-                } else {
-                    throw e;  // only catch RuntimeException:ImageReaderContext is not initialized
-                }
+            if (isImageReaderContextNotInitializedException(e)) {
+                image = null;
+            } else {
+                throw e;  // only catch RuntimeException:ImageReaderContext is not initialized
             }
-
-            if (image == null) {
-                return null;
-            }
-            return new AndroidImageProxy(image);
         }
+
+        if (image == null) {
+            return null;
+        }
+        return new AndroidImageProxy(image);
     }
 
     @Override
     @Nullable
-    public ImageProxy acquireNextImage() {
-        synchronized (mLock) {
-            Image image;
-            try {
-                image = mImageReader.acquireNextImage();
-            } catch (RuntimeException e) {
+    public synchronized ImageProxy acquireNextImage() {
+        Image image;
+        try {
+            image = mImageReader.acquireNextImage();
+        } catch (RuntimeException e) {
             /* In API level 23 or below,  it will throw "java.lang.RuntimeException:
                ImageReaderContext is not initialized" when ImageReader is closed. To make the
                behavior consistent as newer API levels,  we make it return null Image instead.*/
-                if (isImageReaderContextNotInitializedException(e)) {
-                    image = null;
-                } else {
-                    throw e;  // only catch RuntimeException:ImageReaderContext is not initialized
-                }
+            if (isImageReaderContextNotInitializedException(e)) {
+                image = null;
+            } else {
+                throw e;  // only catch RuntimeException:ImageReaderContext is not initialized
             }
-
-            if (image == null) {
-                return null;
-            }
-            return new AndroidImageProxy(image);
         }
+
+        if (image == null) {
+            return null;
+        }
+        return new AndroidImageProxy(image);
     }
 
     private boolean isImageReaderContextNotInitializedException(RuntimeException e) {
@@ -106,66 +101,50 @@
     }
 
     @Override
-    public void close() {
-        synchronized (mLock) {
-            mImageReader.close();
-        }
+    public synchronized void close() {
+        mImageReader.close();
     }
 
     @Override
-    public int getHeight() {
-        synchronized (mLock) {
-            return mImageReader.getHeight();
-        }
+    public synchronized int getHeight() {
+        return mImageReader.getHeight();
     }
 
     @Override
-    public int getWidth() {
-        synchronized (mLock) {
-            return mImageReader.getWidth();
-        }
+    public synchronized int getWidth() {
+        return mImageReader.getWidth();
     }
 
     @Override
-    public int getImageFormat() {
-        synchronized (mLock) {
-            return mImageReader.getImageFormat();
-        }
+    public synchronized int getImageFormat() {
+        return mImageReader.getImageFormat();
     }
 
     @Override
-    public int getMaxImages() {
-        synchronized (mLock) {
-            return mImageReader.getMaxImages();
-        }
+    public synchronized int getMaxImages() {
+        return mImageReader.getMaxImages();
     }
 
     @Nullable
     @Override
-    public Surface getSurface() {
-        synchronized (mLock) {
-            return mImageReader.getSurface();
-        }
+    public synchronized Surface getSurface() {
+        return mImageReader.getSurface();
     }
 
     @Override
-    public void setOnImageAvailableListener(
+    public synchronized void setOnImageAvailableListener(
             @NonNull OnImageAvailableListener listener,
             @NonNull Executor executor) {
-        synchronized (mLock) {
-            // ImageReader does not accept an executor. As a workaround, the callback is run on main
-            // handler then immediately posted to the executor.
-            ImageReader.OnImageAvailableListener transformedListener = (imageReader) ->
-                    executor.execute(() -> listener.onImageAvailable(AndroidImageReaderProxy.this));
-            mImageReader.setOnImageAvailableListener(transformedListener,
-                    MainThreadAsyncHandler.getInstance());
-        }
+        // ImageReader does not accept an executor. As a workaround, the callback is run on main
+        // handler then immediately posted to the executor.
+        ImageReader.OnImageAvailableListener transformedListener = (imageReader) ->
+                executor.execute(() -> listener.onImageAvailable(AndroidImageReaderProxy.this));
+        mImageReader.setOnImageAvailableListener(transformedListener,
+                MainThreadAsyncHandler.getInstance());
     }
 
     @Override
-    public void clearOnImageAvailableListener() {
-        synchronized (mLock) {
-            mImageReader.setOnImageAvailableListener(null, null);
-        }
+    public synchronized void clearOnImageAvailableListener() {
+        mImageReader.setOnImageAvailableListener(null, null);
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index b1b4c0c..053af09 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -31,7 +31,6 @@
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_MAX_CAPTURE_STAGES;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_MAX_RESOLUTION;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SESSION_CONFIG_UNPACKER;
-import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SESSION_PROCESSOR_ENABLED;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SUPPORTED_RESOLUTIONS;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_TARGET_ASPECT_RATIO;
@@ -91,7 +90,6 @@
 import androidx.camera.core.impl.ImmediateSurface;
 import androidx.camera.core.impl.MutableConfig;
 import androidx.camera.core.impl.MutableOptionsBundle;
-import androidx.camera.core.impl.MutableTagBundle;
 import androidx.camera.core.impl.OptionsBundle;
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.UseCaseConfig;
@@ -312,11 +310,6 @@
      */
     private boolean mUseSoftwareJpeg = false;
 
-    /**
-     * Whether SessionProcessor is enabled.
-     */
-    private boolean mIsSessionProcessorEnabled = true;
-
     ////////////////////////////////////////////////////////////////////////////////////////////
     // [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
     ////////////////////////////////////////////////////////////////////////////////////////////
@@ -390,54 +383,6 @@
                                     resolution.getHeight(), getImageFormat(), MAX_IMAGES, 0));
             mMetadataMatchingCaptureCallback = new CameraCaptureCallback() {
             };
-        } else if (mIsSessionProcessorEnabled) {
-            ImageReaderProxy imageReader;
-            if (getImageFormat() == ImageFormat.JPEG) {
-                imageReader =
-                        new AndroidImageReaderProxy(ImageReader.newInstance(resolution.getWidth(),
-                                resolution.getHeight(), getImageFormat(), MAX_IMAGES));
-            } else if (getImageFormat() == ImageFormat.YUV_420_888) { // convert it into Jpeg
-                if (Build.VERSION.SDK_INT >= 26) {
-                    // Jpeg rotation / quality will be set to softwareJpegProcessor later in
-                    // ImageCaptureRequestProcessor.
-                    mYuvToJpegProcessor =
-                            new YuvToJpegProcessor(getJpegQualityInternal(), MAX_IMAGES);
-
-                    ModifiableImageReaderProxy inputReader =
-                            new ModifiableImageReaderProxy(
-                                    ImageReader.newInstance(resolution.getWidth(),
-                                            resolution.getHeight(),
-                                            ImageFormat.YUV_420_888,
-                                            MAX_IMAGES));
-
-                    CaptureBundle captureBundle = CaptureBundles.singleDefaultCaptureBundle();
-                    ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(
-                            inputReader,
-                            captureBundle,
-                            mYuvToJpegProcessor
-                    ).setPostProcessExecutor(mExecutor).setOutputFormat(ImageFormat.JPEG).build();
-
-                    // Ensure the ImageProxy contains the same capture stage id expected from the
-                    // ProcessingImageReader.
-                    MutableTagBundle tagBundle = MutableTagBundle.create();
-                    tagBundle.putTag(processingImageReader.getTagBundleKey(),
-                            captureBundle.getCaptureStages().get(0).getId());
-                    inputReader.setImageTagBundle(tagBundle);
-
-                    YuvToJpegProcessor processorToClose = mYuvToJpegProcessor;
-                    processingImageReader.getCloseFuture().addListener(() -> {
-                        processorToClose.close();
-                    }, CameraXExecutors.directExecutor());
-
-                    imageReader = processingImageReader;
-                } else {
-                    throw new UnsupportedOperationException("Does not support API level < 26");
-                }
-            } else {
-                throw new IllegalArgumentException("Unsupported image format:" + getImageFormat());
-            }
-            mMetadataMatchingCaptureCallback = new CameraCaptureCallback() {};
-            mImageReader = new SafeCloseImageReaderProxy(imageReader);
         } else if (mCaptureProcessor != null || mUseSoftwareJpeg) {
             // Capture processor set from configuration takes precedence over software JPEG.
             CaptureProcessor captureProcessor = mCaptureProcessor;
@@ -468,14 +413,11 @@
             }
 
             // TODO: To allow user to use an Executor for the image processing.
-            mProcessingImageReader = new ProcessingImageReader.Builder(
-                    resolution.getWidth(),
-                    resolution.getHeight(),
-                    inputFormat,
-                    mMaxCaptureStages,
+            mProcessingImageReader = new ProcessingImageReader.Builder(resolution.getWidth(),
+                    resolution.getHeight(), inputFormat, mMaxCaptureStages,
                     getCaptureBundle(CaptureBundles.singleDefaultCaptureBundle()),
-                    captureProcessor
-            ).setPostProcessExecutor(mExecutor).setOutputFormat(outputFormat).build();
+                    captureProcessor).setPostProcessExecutor(mExecutor).setOutputFormat(
+                    outputFormat).build();
 
             mMetadataMatchingCaptureCallback = mProcessingImageReader.getCameraCaptureCallback();
             mImageReader = new SafeCloseImageReaderProxy(mProcessingImageReader);
@@ -712,21 +654,7 @@
                 builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
                         ImageFormat.YUV_420_888);
             } else {
-                List<Pair<Integer, Size[]>> supportedSizes =
-                        builder.getMutableConfig().retrieveOption(OPTION_SUPPORTED_RESOLUTIONS,
-                                null);
-                if (supportedSizes == null) {
-                    builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.JPEG);
-                } else {
-                    // Use Jpeg first if supported.
-                    if (isImageFormatSupported(supportedSizes, ImageFormat.JPEG)) {
-                        builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
-                                ImageFormat.JPEG);
-                    } else if (isImageFormatSupported(supportedSizes, ImageFormat.YUV_420_888)) {
-                        builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
-                                ImageFormat.YUV_420_888);
-                    }
-                }
+                builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT, ImageFormat.JPEG);
             }
         }
 
@@ -737,18 +665,6 @@
         return builder.getUseCaseConfig();
     }
 
-    private static boolean isImageFormatSupported(List<Pair<Integer, Size[]>> supportedSizes,
-            int imageFormat) {
-        if (supportedSizes == null) {
-            return false;
-        }
-        for (Pair<Integer, Size[]> supportedSize : supportedSizes) {
-            if (supportedSize.first.equals(imageFormat)) {
-                return true;
-            }
-        }
-        return false;
-    }
     /**
      * Configures flash mode to CameraControlInternal once it is ready.
      *
@@ -1638,7 +1554,6 @@
         // This will only be set to true if software JPEG was requested and
         // enforceSoftwareJpegConstraints() hasn't removed the request.
         mUseSoftwareJpeg = useCaseConfig.isSoftwareJpegEncoderRequested();
-        mIsSessionProcessorEnabled = useCaseConfig.isSessionProcessorEnabled();
 
         CameraInternal camera = getCamera();
         Preconditions.checkNotNull(camera, "Attached camera cannot be null");
@@ -2591,17 +2506,6 @@
         }
 
         /**
-         * Set the flag to indicate whether SessionProcessor is enabled.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setSessionProcessorEnabled(boolean enabled) {
-            getMutableConfig().insertOption(OPTION_SESSION_PROCESSOR_ENABLED, enabled);
-            return this;
-        }
-
-        /**
          * Sets the max number of {@link CaptureStage}.
          *
          * @param maxCaptureStages The max CaptureStage number.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ModifiableImageReaderProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/ModifiableImageReaderProxy.java
deleted file mode 100644
index 4d9250b..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/ModifiableImageReaderProxy.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import android.graphics.Matrix;
-import android.media.ImageReader;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.TagBundle;
-
-/**
- * An ImageReaderProxy implementation that allows to modify the ImageInfo data of the images
- * retrieved.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-class ModifiableImageReaderProxy extends AndroidImageReaderProxy {
-    private volatile TagBundle mTagBundle = null;
-    private volatile Long mTimestamp = null;
-    private volatile Integer mRotationDegrees = null;
-    private volatile Matrix mSensorToBufferTransformMatrix = null;
-
-    ModifiableImageReaderProxy(@NonNull ImageReader imageReader) {
-        super(imageReader);
-    }
-
-    void setImageTagBundle(@NonNull TagBundle tagBundle) {
-        mTagBundle = tagBundle;
-    }
-
-    void setImageTimeStamp(long timestamp) {
-        mTimestamp = timestamp;
-    }
-
-    void setImageRotationDegrees(int rotationDegrees) {
-        mRotationDegrees = rotationDegrees;
-    }
-
-    void setImageSensorToBufferTransformaMatrix(@NonNull Matrix matrix) {
-        mSensorToBufferTransformMatrix = matrix;
-    }
-
-    @Nullable
-    @Override
-    public ImageProxy acquireLatestImage() {
-        return modifyImage(super.acquireNextImage());
-    }
-
-    @Nullable
-    @Override
-    public ImageProxy acquireNextImage() {
-        return modifyImage(super.acquireNextImage());
-    }
-
-    private ImageProxy modifyImage(ImageProxy imageProxy) {
-        ImageInfo origin = imageProxy.getImageInfo();
-        ImageInfo  imageInfo = ImmutableImageInfo.create(
-                mTagBundle != null ? mTagBundle : origin.getTagBundle(),
-                mTimestamp != null ? mTimestamp.longValue() : origin.getTimestamp(),
-                mRotationDegrees != null ? mRotationDegrees.intValue() :
-                        origin.getRotationDegrees(),
-                mSensorToBufferTransformMatrix != null ? mSensorToBufferTransformMatrix :
-                        origin.getSensorToBufferTransformMatrix());
-        return new SettableImageProxy(imageProxy, imageInfo);
-    }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index bf4d35b..9a92907 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -26,7 +26,6 @@
 import static androidx.camera.core.impl.PreviewConfig.OPTION_DEFAULT_SESSION_CONFIG;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_MAX_RESOLUTION;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_PREVIEW_CAPTURE_PROCESSOR;
-import static androidx.camera.core.impl.PreviewConfig.OPTION_RGBA8888_SURFACE_REQUIRED;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_SESSION_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_SUPPORTED_RESOLUTIONS;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
@@ -205,9 +204,8 @@
             mSessionDeferrableSurface.close();
         }
 
-        boolean isRGBA8888SurfaceRequired = config.isRgba8888SurfaceRequired(false);
         final SurfaceRequest surfaceRequest = new SurfaceRequest(resolution, getCamera(),
-                isRGBA8888SurfaceRequired);
+                captureProcessor != null);
         mCurrentSurfaceRequest = surfaceRequest;
 
         if (sendSurfaceRequestIfReady()) {
@@ -1033,18 +1031,6 @@
             return this;
         }
 
-        /**
-         * Sets if the surface requires RGBA8888 format.
-         * @hide
-         */
-        @RestrictTo(Scope.LIBRARY_GROUP)
-        @NonNull
-        public Builder setIsRgba8888SurfaceRequired(boolean isRgba8888SurfaceRequired) {
-            getMutableConfig().insertOption(
-                    OPTION_RGBA8888_SURFACE_REQUIRED, isRgba8888SurfaceRequired);
-            return this;
-        }
-
         /** @hide */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingImageReader.java b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingImageReader.java
index 406ba95..e702f3b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingImageReader.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingImageReader.java
@@ -144,7 +144,7 @@
     boolean mProcessing = false;
 
     @GuardedBy("mLock")
-    final ImageReaderProxy mInputImageReader;
+    final MetadataImageReader mInputImageReader;
 
     @GuardedBy("mLock")
     final ImageReaderProxy mOutputImageReader;
@@ -403,11 +403,7 @@
     @Nullable
     CameraCaptureCallback getCameraCaptureCallback() {
         synchronized (mLock) {
-            if (mInputImageReader instanceof MetadataImageReader) {
-                return ((MetadataImageReader) mInputImageReader).getCameraCaptureCallback();
-            } else {
-                return new CameraCaptureCallback() {};
-            }
+            return mInputImageReader.getCameraCaptureCallback();
         }
     }
 
@@ -458,7 +454,7 @@
      */
     static final class Builder {
         @NonNull
-        protected final ImageReaderProxy mInputImageReader;
+        protected final MetadataImageReader mInputImageReader;
         @NonNull
         protected final CaptureBundle mCaptureBundle;
         @NonNull
@@ -477,7 +473,7 @@
          * @param captureProcessor The {@link CaptureProcessor} to be invoked when the Images are
          *                         ready
          */
-        Builder(@NonNull ImageReaderProxy imageReader, @NonNull CaptureBundle captureBundle,
+        Builder(@NonNull MetadataImageReader imageReader, @NonNull CaptureBundle captureBundle,
                 @NonNull CaptureProcessor captureProcessor) {
             mInputImageReader = imageReader;
             mCaptureBundle = captureBundle;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java
index fc37406..0f233fb 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageCaptureConfig.java
@@ -62,8 +62,6 @@
             Option.create("camerax.core.imageCapture.flashType", int.class);
     public static final Option<Integer> OPTION_JPEG_COMPRESSION_QUALITY =
             Option.create("camerax.core.imageCapture.jpegCompressionQuality", int.class);
-    public static final Option<Boolean> OPTION_SESSION_PROCESSOR_ENABLED =
-            Option.create("camerax.core.imageCapture.sessionProcessorEnabled", boolean.class);
 
     // *********************************************************************************************
 
@@ -290,13 +288,6 @@
         return retrieveOption(OPTION_JPEG_COMPRESSION_QUALITY);
     }
 
-    /**
-     * Returns if the SessionProcessor is enabled.
-     */
-    public boolean isSessionProcessorEnabled() {
-        return retrieveOption(OPTION_SESSION_PROCESSOR_ENABLED, false);
-    }
-
     // Implementations of IO default methods
 
     /**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java
index 6d235ab..146c52a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/PreviewConfig.java
@@ -37,8 +37,6 @@
             "camerax.core.preview.imageInfoProcessor", ImageInfoProcessor.class);
     public static final Option<CaptureProcessor> OPTION_PREVIEW_CAPTURE_PROCESSOR =
             Option.create("camerax.core.preview.captureProcessor", CaptureProcessor.class);
-    public static final Option<Boolean> OPTION_RGBA8888_SURFACE_REQUIRED =
-            Option.create("camerax.core.preview.isRgba8888SurfaceRequired", Boolean.class);
     private final OptionsBundle mConfig;
 
     /** Creates a new configuration instance. */
@@ -91,12 +89,6 @@
     }
 
     /**
-     * Returns if the preview surface requires RGBA8888 format.
-     */
-    public boolean isRgba8888SurfaceRequired(boolean valueIfMissing) {
-        return retrieveOption(OPTION_RGBA8888_SURFACE_REQUIRED, valueIfMissing);
-    }
-    /**
      * Retrieves the format of the image that is fed as input.
      *
      * <p>This should be YUV_420_888, when processing is run on the image. Otherwise it is PRIVATE.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
index 70bdbc4..db0e00f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
@@ -16,6 +16,8 @@
 
 package androidx.camera.core.impl;
 
+import android.util.Range;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
@@ -75,6 +77,13 @@
     Option<CameraSelector> OPTION_CAMERA_SELECTOR =
             Config.Option.create("camerax.core.useCase.cameraSelector", CameraSelector.class);
 
+    /**
+     * Option: camerax.core.useCase.targetFramerate
+     */
+    Option<Range<Integer>> OPTION_TARGET_FRAME_RATE =
+            Config.Option.create("camerax.core.useCase.targetFrameRate", CameraSelector.class);
+
+
     // *********************************************************************************************
 
     /**
@@ -250,6 +259,28 @@
     }
 
     /**
+     * Retrieves target frame rate
+     * @param valueIfMissing
+     * @return the stored value or <code>valueIfMissing</code> if the value does not exist in
+     * this configuration
+     */
+    @Nullable
+    default Range<Integer> getTargetFramerate(@Nullable Range<Integer> valueIfMissing) {
+        return retrieveOption(OPTION_TARGET_FRAME_RATE, valueIfMissing);
+    }
+
+    /**
+     * Retrieves the target frame rate
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    @NonNull
+    default Range<Integer> getTargetFramerate() {
+        return retrieveOption(OPTION_TARGET_FRAME_RATE);
+    }
+
+    /**
      * Builder for a {@link UseCase}.
      *
      * @param <T> The type of the object which will be built by {@link #build()}.
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/UseCaseConfigTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/UseCaseConfigTest.kt
new file mode 100644
index 0000000..0f6df49
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/UseCaseConfigTest.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.core.impl
+
+import android.os.Build
+import android.util.Range
+import androidx.camera.testing.fakes.FakeUseCaseConfig
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+@RunWith(
+    RobolectricTestRunner::class
+)
+@DoNotInstrument
+class UseCaseConfigTest {
+    @Test
+    fun canGetTargetFrameRate() {
+        val useCaseBuilder = FakeUseCaseConfig.Builder()
+        val range = Range(10, 20)
+        useCaseBuilder.mutableConfig.insertOption(UseCaseConfig.OPTION_TARGET_FRAME_RATE, range)
+        Truth.assertThat(useCaseBuilder.useCaseConfig.targetFramerate).isEqualTo(range)
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java
index b7927fb..4204cc3 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionsInfo.java
@@ -17,6 +17,9 @@
 package androidx.camera.extensions;
 
 
+import android.content.Context;
+import android.hardware.camera2.CameraCharacteristics;
+import android.util.Pair;
 import android.util.Range;
 import android.util.Size;
 
@@ -42,7 +45,9 @@
 import androidx.camera.extensions.internal.VendorExtender;
 import androidx.camera.extensions.internal.Version;
 
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 /**
  * A class for querying extensions related information.
@@ -227,6 +232,11 @@
     private static VendorExtender getVendorExtender(int mode) {
         boolean isAdvancedExtenderSupported = isAdvancedExtenderSupported();
 
+        // Disable Advanced Extender until it is well tested.
+        if (isAdvancedExtenderSupported) {
+            return new DisabledVendorExtender();
+        }
+
         VendorExtender vendorExtender;
         if (isAdvancedExtenderSupported) {
             vendorExtender = new AdvancedVendorExtender(mode);
@@ -270,4 +280,47 @@
         }
         return id;
     }
+
+    static class DisabledVendorExtender implements VendorExtender {
+        @Override
+        public boolean isExtensionAvailable(@NonNull String cameraId,
+                @NonNull Map<String, CameraCharacteristics> characteristicsMap) {
+            return false;
+        }
+
+        @Override
+        public void init(@NonNull CameraInfo cameraInfo) {
+
+        }
+
+        @Nullable
+        @Override
+        public Range<Long> getEstimatedCaptureLatencyRange(@Nullable Size size) {
+            return null;
+        }
+
+        @NonNull
+        @Override
+        public List<Pair<Integer, Size[]>> getSupportedPreviewOutputResolutions() {
+            return Collections.emptyList();
+        }
+
+        @NonNull
+        @Override
+        public List<Pair<Integer, Size[]>> getSupportedCaptureOutputResolutions() {
+            return Collections.emptyList();
+        }
+
+        @NonNull
+        @Override
+        public Size[] getSupportedYuvAnalysisResolutions() {
+            return new Size[0];
+        }
+
+        @Nullable
+        @Override
+        public SessionProcessor createSessionProcessor(@NonNull Context context) {
+            return null;
+        }
+    }
 }
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
index a11ff2b..72a78cf 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
@@ -180,11 +180,16 @@
         return map.getOutputSizes(imageFormat);
     }
 
-    private int getPreviewOutputImageFormat() {
-        return ImageFormat.PRIVATE;
+    private int getPreviewInputImageFormat() {
+        if (mPreviewExtenderImpl != null && mPreviewExtenderImpl.getProcessorType()
+                == PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_IMAGE_PROCESSOR) {
+            return ImageFormat.YUV_420_888;
+        } else {
+            return ImageFormat.PRIVATE;
+        }
     }
 
-    private int getCaptureOutputImageFormat() {
+    private int getCaptureInputImageFormat() {
         if (mImageCaptureExtenderImpl != null
                 && mImageCaptureExtenderImpl.getCaptureProcessor() != null) {
             return ImageFormat.YUV_420_888;
@@ -213,7 +218,7 @@
         // Returns output sizes from stream configuration map if OEM returns null or OEM does not
         // implement the function. It is required to return all supported sizes so it must fetch
         // all sizes from the stream configuration map here.
-        int imageformat = getPreviewOutputImageFormat();
+        int imageformat = getPreviewInputImageFormat();
         return Arrays.asList(new Pair<>(imageformat, getOutputSizes(imageformat)));
     }
 
@@ -237,7 +242,7 @@
         // Returns output sizes from stream configuration map if OEM returns null or OEM does not
         // implement the function. It is required to return all supported sizes so it must fetch
         // all sizes from the stream configuration map here.
-        int imageFormat = getCaptureOutputImageFormat();
+        int imageFormat = getCaptureInputImageFormat();
         return Arrays.asList(new Pair<>(imageFormat, getOutputSizes(imageFormat)));
     }
 
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
index a09bf72..c280363 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
@@ -118,8 +118,6 @@
             } else {
                 Logger.e(TAG, "ImageCaptureExtenderImpl is null!");
             }
-        } else { // Advanced vendor interface
-            builder.setSessionProcessorEnabled(true);
         }
 
         builder.getMutableConfig().insertOption(OPTION_IMAGE_CAPTURE_CONFIG_PROVIDER_MODE,
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
index 9fa8a86..3da8a12 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
@@ -105,7 +105,6 @@
                                 AdaptingPreviewProcessor(
                                 (PreviewImageProcessorImpl) previewExtenderImpl.getProcessor());
                         builder.setCaptureProcessor(adaptingPreviewProcessor);
-                        builder.setIsRgba8888SurfaceRequired(true);
                         previewEventAdapter = new PreviewEventAdapter(previewExtenderImpl, context,
                                 adaptingPreviewProcessor);
                         break;
@@ -119,10 +118,6 @@
             } else {
                 Logger.e(TAG, "PreviewExtenderImpl is null!");
             }
-        } else { // Advanced extensions interface.
-            // Set RGB8888 = true always since we have no way to tell if the OEM implementation does
-            // the processing or not.
-            builder.setIsRgba8888SurfaceRequired(true);
         }
 
         builder.getMutableConfig().insertOption(OPTION_PREVIEW_CONFIG_PROVIDER_MODE, effectMode);
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java b/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java
index 110695c..e9443ca 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/SurfaceTextureProvider.java
@@ -101,30 +101,6 @@
     }
 
     /**
-     * Creates a {@link Preview.SurfaceProvider} that is backed by a {@link SurfaceTexture} which
-     * is suitable to be used in testing that doesn't actually show camera preview but just need
-     * a surface for preview.
-     *
-     * <p> The {@link SurfaceTexture} will be released when it is no longer needed.
-     *
-     */
-    @NonNull
-    public static Preview.SurfaceProvider createSurfaceTextureProvider() {
-        return createSurfaceTextureProvider(new SurfaceTextureCallback() {
-            @Override
-            public void onSurfaceTextureReady(@NonNull SurfaceTexture surfaceTexture,
-                    @NonNull Size resolution) {
-                // no op
-            }
-
-            @Override
-            public void onSafeToRelease(@NonNull SurfaceTexture surfaceTexture) {
-                surfaceTexture.release();
-            }
-        });
-    }
-
-    /**
      * Creates a {@link Preview.SurfaceProvider} that is backed by a {@link SurfaceTexture}.
      *
      * <p>This method also creates a backing OpenGL thread that will automatically drain frames
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
deleted file mode 100644
index f862765..0000000
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSessionProcessor.kt
+++ /dev/null
@@ -1,396 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.testing.fakes
-
-import android.hardware.camera2.CameraDevice
-import android.media.ImageReader
-import android.media.ImageWriter
-import android.os.Handler
-import android.os.Looper
-import android.os.SystemClock
-import android.view.Surface
-import androidx.annotation.RequiresApi
-import androidx.camera.core.CameraInfo
-import androidx.camera.core.impl.CameraCaptureFailure
-import androidx.camera.core.impl.CameraCaptureResult
-import androidx.camera.core.impl.Config
-import androidx.camera.core.impl.DeferrableSurface
-import androidx.camera.core.impl.OptionsBundle
-import androidx.camera.core.impl.OutputSurface
-import androidx.camera.core.impl.RequestProcessor
-import androidx.camera.core.impl.SessionConfig
-import androidx.camera.core.impl.SessionProcessor
-import androidx.camera.core.impl.SessionProcessorSurface
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.withTimeout
-import kotlinx.coroutines.withTimeoutOrNull
-
-const val FAKE_CAPTURE_SEQUENCE_ID = 1
-
-@RequiresApi(23)
-class FakeSessionProcessor(
-    val inputFormatPreview: Int?,
-    val inputFormatCapture: Int?
-) : SessionProcessor {
-    private lateinit var previewProcessorSurface: DeferrableSurface
-    private lateinit var captureProcessorSurface: DeferrableSurface
-    private var intermediaPreviewImageReader: ImageReader? = null
-    private var intermediaCaptureImageReader: ImageReader? = null
-    private var intermediaPreviewImageWriter: ImageWriter? = null
-    private var intermediaCaptureImageWriter: ImageWriter? = null
-
-    private val previewOutputConfigId = 1
-    private val captureOutputConfigId = 2
-
-    private var requestProcessor: RequestProcessor? = null
-
-    // Values of these Deferred are the timestamp to complete.
-    private val initSessionCalled = CompletableDeferred<Long>()
-    private val deInitSessionCalled = CompletableDeferred<Long>()
-    private val onCaptureSessionStartCalled = CompletableDeferred<Long>()
-    private val onCaptureSessionEndCalled = CompletableDeferred<Long>()
-    private val startRepeatingCalled = CompletableDeferred<Long>()
-    private val startCaptureCalled = CompletableDeferred<Long>()
-    private val setParametersCalled = CompletableDeferred<Config>()
-    private var latestParameters: Config = OptionsBundle.emptyBundle()
-    private var blockRunAfterInitSession: () -> Unit = {}
-
-    fun releaseSurfaces() {
-        intermediaPreviewImageReader?.close()
-        intermediaCaptureImageReader?.close()
-    }
-
-    fun runAfterInitSession(block: () -> Unit) {
-        blockRunAfterInitSession = block
-    }
-
-    override fun initSession(
-        cameraInfo: CameraInfo,
-        previewSurfaceConfig: OutputSurface,
-        imageCaptureSurfaceConfig: OutputSurface,
-        imageAnalysisSurfaceConfig: OutputSurface?
-    ): SessionConfig {
-        initSessionCalled.complete(SystemClock.elapsedRealtimeNanos())
-        val handler = Handler(Looper.getMainLooper())
-
-        var sessionBuilder = SessionConfig.Builder()
-
-        // Preview
-        lateinit var previewTransformedSurface: Surface
-        if (inputFormatPreview == null) { // no conversion, use origin surface.
-            previewTransformedSurface = previewSurfaceConfig.surface
-        } else {
-            intermediaPreviewImageReader = ImageReader.newInstance(
-                640, 480,
-                inputFormatPreview, 2
-            )
-            previewTransformedSurface = intermediaPreviewImageReader!!.surface
-
-            intermediaPreviewImageWriter = ImageWriter.newInstance(
-                previewSurfaceConfig.surface, 2
-            )
-
-            intermediaPreviewImageReader!!.setOnImageAvailableListener(
-                {
-                    it.acquireNextImage().use {
-                        val imageDequeued = intermediaPreviewImageWriter!!.dequeueInputImage()
-                        intermediaPreviewImageWriter!!.queueInputImage(imageDequeued)
-                    }
-                },
-                handler
-            )
-        }
-        previewProcessorSurface =
-            SessionProcessorSurface(previewTransformedSurface, previewOutputConfigId)
-        previewProcessorSurface.terminationFuture.addListener(
-            {
-                intermediaPreviewImageReader?.close()
-                intermediaPreviewImageWriter?.close()
-            },
-            CameraXExecutors.directExecutor()
-        )
-        sessionBuilder.addSurface(previewProcessorSurface)
-
-        // Capture
-        lateinit var captureTransformedSurface: Surface
-        if (inputFormatCapture == null) { // no conversion, use origin surface.
-            captureTransformedSurface = imageCaptureSurfaceConfig.surface
-        } else {
-            intermediaCaptureImageReader = ImageReader.newInstance(
-                640, 480,
-                inputFormatCapture, 2
-            )
-            captureTransformedSurface = intermediaCaptureImageReader!!.surface
-
-            intermediaCaptureImageWriter = ImageWriter.newInstance(
-                imageCaptureSurfaceConfig.surface, 2
-            )
-
-            intermediaCaptureImageReader!!.setOnImageAvailableListener(
-                {
-                    it.acquireNextImage().use {
-                        val imageDequeued = intermediaCaptureImageWriter!!.dequeueInputImage()
-                        intermediaCaptureImageWriter!!.queueInputImage(imageDequeued)
-                    }
-                },
-                handler
-            )
-        }
-        captureProcessorSurface =
-            SessionProcessorSurface(captureTransformedSurface, captureOutputConfigId)
-
-        captureProcessorSurface.terminationFuture.addListener(
-            {
-                intermediaCaptureImageReader?.close()
-                intermediaCaptureImageWriter?.close()
-            },
-            CameraXExecutors.directExecutor()
-        )
-        sessionBuilder.addSurface(captureProcessorSurface)
-
-        sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG)
-        val sessionConfig = sessionBuilder.build()
-        blockRunAfterInitSession()
-        return sessionConfig
-    }
-
-    override fun deInitSession() {
-        deInitSessionCalled.complete(SystemClock.elapsedRealtimeNanos())
-        previewProcessorSurface.close()
-        captureProcessorSurface.close()
-    }
-
-    override fun setParameters(config: Config) {
-        setParametersCalled.complete(config)
-        latestParameters = config
-    }
-
-    override fun onCaptureSessionStart(_requestProcessor: RequestProcessor) {
-        onCaptureSessionStartCalled.complete(SystemClock.elapsedRealtimeNanos())
-        requestProcessor = _requestProcessor
-    }
-
-    override fun onCaptureSessionEnd() {
-        onCaptureSessionEndCalled.complete(SystemClock.elapsedRealtimeNanos())
-    }
-
-    fun getLatestParameters(): Config {
-        return latestParameters
-    }
-
-    override fun startRepeating(callback: SessionProcessor.CaptureCallback): Int {
-        startRepeatingCalled.complete(SystemClock.elapsedRealtimeNanos())
-        val builder = RequestProcessorRequest.Builder().apply {
-            addTargetOutputConfigId(previewOutputConfigId)
-            setParameters(latestParameters)
-            setTemplateId(CameraDevice.TEMPLATE_PREVIEW)
-        }
-
-        requestProcessor!!.setRepeating(
-            builder.build(),
-            object : RequestProcessor.Callback {
-                override fun onCaptureStarted(
-                    request: RequestProcessor.Request,
-                    frameNumber: Long,
-                    timestamp: Long
-                ) {}
-
-                override fun onCaptureProgressed(
-                    request: RequestProcessor.Request,
-                    captureResult: CameraCaptureResult
-                ) {}
-
-                override fun onCaptureCompleted(
-                    request: RequestProcessor.Request,
-                    captureResult: CameraCaptureResult
-                ) {
-                    callback.onCaptureSequenceCompleted(1)
-                }
-
-                override fun onCaptureFailed(
-                    request: RequestProcessor.Request,
-                    captureFailure: CameraCaptureFailure
-                ) {}
-
-                override fun onCaptureBufferLost(
-                    request: RequestProcessor.Request,
-                    frameNumber: Long,
-                    outputConfigId: Int
-                ) {}
-
-                override fun onCaptureSequenceCompleted(
-                    sequenceId: Int,
-                    frameNumber: Long
-                ) {}
-
-                override fun onCaptureSequenceAborted(sequenceId: Int) {
-                }
-            }
-        )
-        return FAKE_CAPTURE_SEQUENCE_ID
-    }
-
-    override fun stopRepeating() {
-    }
-
-    override fun startCapture(callback: SessionProcessor.CaptureCallback): Int {
-        startCaptureCalled.complete(SystemClock.elapsedRealtimeNanos())
-        val request = RequestProcessorRequest.Builder().apply {
-            addTargetOutputConfigId(captureOutputConfigId)
-            setParameters(latestParameters)
-            setTemplateId(CameraDevice.TEMPLATE_STILL_CAPTURE)
-        }.build()
-
-        requestProcessor!!.submit(
-            request,
-            object : RequestProcessor.Callback {
-                override fun onCaptureCompleted(
-                    request: RequestProcessor.Request,
-                    captureResult: CameraCaptureResult
-                ) {
-                    callback.onCaptureSequenceCompleted(1)
-                }
-
-                override fun onCaptureStarted(
-                    request: RequestProcessor.Request,
-                    frameNumber: Long,
-                    timestamp: Long
-                ) {}
-
-                override fun onCaptureProgressed(
-                    request: RequestProcessor.Request,
-                    captureResult: CameraCaptureResult
-                ) {}
-
-                override fun onCaptureFailed(
-                    request: RequestProcessor.Request,
-                    captureFailure: CameraCaptureFailure
-                ) {
-                    callback.onCaptureFailed(1)
-                }
-
-                override fun onCaptureBufferLost(
-                    request: RequestProcessor.Request,
-                    frameNumber: Long,
-                    outputConfigId: Int
-                ) {}
-
-                override fun onCaptureSequenceCompleted(sequenceId: Int, frameNumber: Long) {}
-
-                override fun onCaptureSequenceAborted(sequenceId: Int) {}
-            }
-        )
-        return FAKE_CAPTURE_SEQUENCE_ID
-    }
-
-    override fun abortCapture(captureSequenceId: Int) {
-    }
-
-    suspend fun assertInitSessionInvoked(): Long {
-        return initSessionCalled.awaitWithTimeout(3000)
-    }
-
-    suspend fun wasInitSessionInvoked(): Boolean {
-        val result = withTimeoutOrNull(3000) { initSessionCalled.await() }
-        return result != null
-    }
-
-    suspend fun assertDeInitSessionInvoked(): Long {
-        return deInitSessionCalled.awaitWithTimeout(3000)
-    }
-
-    suspend fun assertOnCaptureSessionStartInvoked(): Long {
-        return onCaptureSessionStartCalled.awaitWithTimeout(3000)
-    }
-
-    suspend fun wasOnCaptureSessionStartInvoked(): Boolean {
-        val result = withTimeoutOrNull(3000) { onCaptureSessionStartCalled.await() }
-        return result != null
-    }
-
-    suspend fun assertOnCaptureEndInvoked(): Long {
-        return onCaptureSessionEndCalled.awaitWithTimeout(3000)
-    }
-
-    suspend fun assertStartRepeatingInvoked(): Long {
-        return startRepeatingCalled.awaitWithTimeout(3000)
-    }
-
-    suspend fun assertStartCaptureInvoked(): Long {
-        return startCaptureCalled.awaitWithTimeout(3000)
-    }
-
-    suspend fun assertSetParametersInvoked(): Config {
-        return setParametersCalled.awaitWithTimeout(3000)
-    }
-
-    private suspend fun <T> Deferred<T>.awaitWithTimeout(timeMillis: Long): T {
-        return withTimeout(timeMillis) {
-            await()
-        }
-    }
-}
-
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-internal class RequestProcessorRequest(
-    private val targetOutputConfigIds: List<Int>,
-    private val parameters: Config,
-    private val templateId: Int
-) : RequestProcessor.Request {
-    override fun getTargetOutputConfigIds(): List<Int> {
-        return targetOutputConfigIds
-    }
-
-    override fun getParameters(): Config {
-        return parameters
-    }
-
-    override fun getTemplateId(): Int {
-        return templateId
-    }
-
-    class Builder {
-        private var targetOutputConfigIds: MutableList<Int> = ArrayList()
-        private var parameters: Config = OptionsBundle.emptyBundle()
-        private var templateId = CameraDevice.TEMPLATE_PREVIEW
-
-        fun addTargetOutputConfigId(targetOutputConfigId: Int): Builder {
-            targetOutputConfigIds.add(targetOutputConfigId)
-            return this
-        }
-
-        fun setParameters(parameters: Config): Builder {
-            this.parameters = parameters
-            return this
-        }
-
-        fun setTemplateId(templateId: Int): Builder {
-            this.templateId = templateId
-            return this
-        }
-
-        fun build(): RequestProcessorRequest {
-            return RequestProcessorRequest(
-                targetOutputConfigIds.toList(),
-                OptionsBundle.from(parameters),
-                templateId
-            )
-        }
-    }
-}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 342ffd0..b06614e 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -41,37 +41,26 @@
 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.AspectRatio
 import androidx.camera.core.Camera
-import androidx.camera.core.CameraFilter
-import androidx.camera.core.CameraInfo
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageCaptureException
 import androidx.camera.core.ImageProxy
-import androidx.camera.core.Preview
 import androidx.camera.core.UseCaseGroup
 import androidx.camera.core.ViewPort
-import androidx.camera.core.impl.CameraConfig
 import androidx.camera.core.impl.CaptureBundle
 import androidx.camera.core.impl.CaptureConfig
 import androidx.camera.core.impl.CaptureProcessor
 import androidx.camera.core.impl.CaptureStage
-import androidx.camera.core.impl.Config
-import androidx.camera.core.impl.ExtendedCameraConfigProviderStore
-import androidx.camera.core.impl.Identifier
 import androidx.camera.core.impl.ImageCaptureConfig
 import androidx.camera.core.impl.ImageOutputConfig
 import androidx.camera.core.impl.ImageProxyBundle
-import androidx.camera.core.impl.MutableOptionsBundle
-import androidx.camera.core.impl.SessionProcessor
 import androidx.camera.core.impl.utils.CameraOrientationUtil
 import androidx.camera.core.impl.utils.Exif
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraUtil
-import androidx.camera.testing.SurfaceTextureProvider
 import androidx.camera.testing.fakes.FakeCaptureStage
 import androidx.camera.testing.fakes.FakeLifecycleOwner
-import androidx.camera.testing.fakes.FakeSessionProcessor
 import androidx.core.content.ContextCompat
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
@@ -1414,139 +1403,6 @@
         simpleCaptureProcessor.close()
     }
 
-    @Test
-    @SdkSuppress(minSdkVersion = 29)
-    fun returnJpegImage_whenSessionProcessorIsSet_outputFormantYuv() = runBlocking {
-        skipTestOnCameraPipeConfig()
-
-        val builder = ImageCapture.Builder()
-        val sessionProcessor = FakeSessionProcessor(
-            inputFormatPreview = null, // null means using the same output surface
-            inputFormatCapture = ImageFormat.YUV_420_888
-        )
-
-        val imageCapture = builder
-            .setSessionProcessorEnabled(true)
-            .setSupportedResolutions(
-                listOf(android.util.Pair(ImageFormat.YUV_420_888, arrayOf(Size(640, 480)))))
-            .build()
-
-        val preview = Preview.Builder().build()
-
-        var camera: Camera
-        withContext(Dispatchers.Main) {
-            preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
-            val cameraSelector =
-                getCameraSelectorWithSessionProcessor(BACK_SELECTOR, sessionProcessor)
-            camera = cameraProvider.bindToLifecycle(
-                fakeLifecycleOwner, cameraSelector, imageCapture, preview)
-        }
-
-        val callback = FakeImageCaptureCallback(capturesCount = 1)
-        imageCapture.takePicture(mainExecutor, callback)
-
-        // Wait for the signal that the image has been captured.
-        callback.awaitCapturesAndAssert(capturedImagesCount = 1)
-
-        val imageProperties = callback.results.first()
-
-        // Check the output image rotation degrees value is correct.
-        assertThat(imageProperties.rotationDegrees).isEqualTo(
-            camera.cameraInfo.getSensorRotationDegrees(imageCapture.targetRotation)
-        )
-        // Check the output format is correct.
-        assertThat(imageProperties.format).isEqualTo(ImageFormat.JPEG)
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = 29)
-    fun returnJpegImage_whenSessionProcessorIsSet_outputFormantJpeg() = runBlocking {
-        assumeFalse(
-            "Cuttlefish does not correctly handle Jpeg exif. Unable to test.",
-            Build.MODEL.contains("Cuttlefish")
-        )
-        skipTestOnCameraPipeConfig()
-
-        val builder = ImageCapture.Builder()
-        val sessionProcessor = FakeSessionProcessor(
-            inputFormatPreview = null, // null means using the same output surface
-            inputFormatCapture = null
-        )
-
-        val imageCapture = builder
-            .setSessionProcessorEnabled(true)
-            .setSupportedResolutions(
-                listOf(android.util.Pair(ImageFormat.JPEG, arrayOf(Size(640, 480)))))
-            .build()
-
-        val preview = Preview.Builder().build()
-
-        withContext(Dispatchers.Main) {
-            preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
-            val cameraSelector =
-                getCameraSelectorWithSessionProcessor(BACK_SELECTOR, sessionProcessor)
-            cameraProvider.bindToLifecycle(
-                fakeLifecycleOwner, cameraSelector, imageCapture, preview)
-        }
-
-        val callback = FakeImageCaptureCallback(capturesCount = 1)
-        imageCapture.takePicture(mainExecutor, callback)
-
-        // Wait for the signal that the image has been captured.
-        callback.awaitCapturesAndAssert(capturedImagesCount = 1)
-
-        val imageProperties = callback.results.first()
-
-        // Check the output image rotation degrees value is correct.
-        assertThat(imageProperties.rotationDegrees).isEqualTo(imageProperties.exif!!.rotation)
-
-        // Check the output format is correct.
-        assertThat(imageProperties.format).isEqualTo(ImageFormat.JPEG)
-    }
-
-    private fun getCameraSelectorWithSessionProcessor(
-        cameraSelector: CameraSelector,
-        sessionProcessor: SessionProcessor
-    ): CameraSelector {
-        val identifier = Identifier.create("idStr")
-        ExtendedCameraConfigProviderStore.addConfig(identifier) { _, _ ->
-            object : CameraConfig {
-                override fun getConfig(): Config {
-                    return MutableOptionsBundle.create()
-                }
-
-                override fun getCompatibilityId(): Identifier {
-                    return Identifier.create(0)
-                }
-
-                override fun getSessionProcessor(
-                    valueIfMissing: SessionProcessor?
-                ): SessionProcessor? {
-                    return sessionProcessor
-                }
-
-                override fun getSessionProcessor(): SessionProcessor {
-                    return sessionProcessor
-                }
-            }
-        }
-
-        val builder = CameraSelector.Builder.fromSelector(cameraSelector)
-        builder.addCameraFilter(object : CameraFilter {
-            override fun filter(cameraInfos: MutableList<CameraInfo>): MutableList<CameraInfo> {
-                val newCameraInfos = mutableListOf<CameraInfo>()
-                newCameraInfos.addAll(cameraInfos)
-                return newCameraInfos
-            }
-
-            override fun getIdentifier(): Identifier {
-                return identifier
-            }
-        })
-
-        return builder.build()
-    }
-
     // Output JPEG format image when setting a CaptureProcessor is only enabled for devices that
     // API level is at least 29.
     @Test
diff --git a/car/app/app-automotive/src/main/res/values-ky/strings.xml b/car/app/app-automotive/src/main/res/values-ky/strings.xml
index 192fef2..a27b786 100644
--- a/car/app/app-automotive/src/main/res/values-ky/strings.xml
+++ b/car/app/app-automotive/src/main/res/values-ky/strings.xml
@@ -23,8 +23,8 @@
     <string name="error_message_client_side_error" msgid="3323186720368387787">"Колдонмодо ката кетти. Бул катаны колдонмонун иштеп чыгуучусуна кабарлаңыз"</string>
     <string name="error_message_host_error" msgid="5484419926049675696">"Тутум катасы"</string>
     <string name="error_message_host_connection_lost" msgid="5723205987837759151">"Тутум убактылуу жеткиликсиз"</string>
-    <string name="error_message_host_not_found" msgid="3241065067065670113">"Тутумду жаңыртуу керек"</string>
-    <string name="error_message_host_incompatible" msgid="160406216155183851">"Тутумду жаңыртуу керек"</string>
+    <string name="error_message_host_not_found" msgid="3241065067065670113">"Системаны жаңыртуу керек"</string>
+    <string name="error_message_host_incompatible" msgid="160406216155183851">"Системаны жаңыртуу керек"</string>
     <string name="error_message_multiple_hosts" msgid="2591031904206928207">"Шайкеш келбеген тутум"</string>
     <string name="error_message_unknown_error" msgid="1918523834689044166">"Белгисиз ката"</string>
     <string name="error_message_no_vending" msgid="5866202078252905802">"Унаа кызматтары менен байланышыңыз"</string>
diff --git a/collection/collection/build.gradle b/collection/collection/build.gradle
index bb4dead..eedae0f 100644
--- a/collection/collection/build.gradle
+++ b/collection/collection/build.gradle
@@ -16,16 +16,35 @@
 
 
 import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
 
 plugins {
     id("AndroidXPlugin")
     id("org.jetbrains.kotlin.multiplatform")
 }
 
+// This should be true when building from GitHub, and false when building
+// from AOSP.  Use this to block out any features or code that we're not
+// ready to build yet in AOSP
+
+def boolProperty(name) {
+    return project.providers.gradleProperty(name).getOrNull()?.toBoolean()
+}
+
+def playground = androidx.build.StudioType.isPlayground(project)
+//Native on by default in playground
+def enableNative = boolProperty('androidx.kmp.native.enabled') ?: playground
+
 kotlin {
     jvm {
         withJava()
     }
+    if (enableNative) {
+        macosX64()
+        macosArm64()
+        linuxX64()
+        mingwX64()
+    }
 
     sourceSets {
         commonMain {
@@ -40,6 +59,19 @@
             }
         }
 
+        if (enableNative) {
+            nativeMain
+
+            configure([linuxX64Main, macosX64Main, macosArm64Main, mingwX64Main]) {
+                dependsOn nativeMain
+            }
+
+            nativeTest
+            configure([linuxX64Test, macosX64Test, macosArm64Test, mingwX64Test]) {
+                dependsOn nativeTest
+            }
+        }
+
         jvmMain {
             dependencies {
                 api(libs.kotlinStdlib)
diff --git a/collection/collection/src/jvmMain/kotlin/androidx/collection/CircularArray.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/CircularArray.kt
similarity index 87%
rename from collection/collection/src/jvmMain/kotlin/androidx/collection/CircularArray.kt
rename to collection/collection/src/commonMain/kotlin/androidx/collection/CircularArray.kt
index 80a7b91..6230ddb 100644
--- a/collection/collection/src/jvmMain/kotlin/androidx/collection/CircularArray.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/CircularArray.kt
@@ -16,6 +16,9 @@
 
 package androidx.collection
 
+import androidx.collection.CollectionPlatformUtils.createIndexOutOfBoundsException
+import kotlin.jvm.JvmOverloads
+
 /**
  * CircularArray is a generic circular array data structure that provides O(1) random read, O(1)
  * prepend and O(1) append. The CircularArray automatically grows its capacity when number of added
@@ -40,8 +43,8 @@
 
         // If minCapacity isn't a power of 2, round up to the next highest
         // power of 2.
-        val arrayCapacity: Int = if (Integer.bitCount(minCapacity) != 1) {
-            Integer.highestOneBit(minCapacity - 1) shl 1
+        val arrayCapacity: Int = if (minCapacity.countOneBits() != 1) {
+            (minCapacity - 1).takeHighestOneBit() shl 1
         } else {
             minCapacity
         }
@@ -97,11 +100,11 @@
      * Remove first element from front of the [CircularArray] and return it.
      *
      * @return The element removed.
-     * @throws ArrayIndexOutOfBoundsException if [CircularArray] is empty.
+     * @throws [ArrayIndexOutOfBoundsException] if [CircularArray] is empty (on jvm)
      */
     public fun popFirst(): E {
         if (head == tail) {
-            throw ArrayIndexOutOfBoundsException()
+            throw createIndexOutOfBoundsException()
         }
         val result = elements[head]
         elements[head] = null
@@ -115,11 +118,11 @@
      * Remove last element from end of the [CircularArray] and return it.
      *
      * @return The element removed.
-     * @throws ArrayIndexOutOfBoundsException if [CircularArray] is empty.
+     * @throws [ArrayIndexOutOfBoundsException] if [CircularArray] is empty
      */
     public fun popLast(): E {
         if (head == tail) {
-            throw ArrayIndexOutOfBoundsException()
+            throw createIndexOutOfBoundsException()
         }
         val t = (tail - 1) and capacityBitmask
         val result = elements[t]
@@ -142,14 +145,14 @@
      * is less than or equal to 0.
      *
      * @param count Number of elements to remove.
-     * @throws ArrayIndexOutOfBoundsException if [count] is larger than [size]
+     * @throws [ArrayIndexOutOfBoundsException] if [count] is larger than [size]
      */
     public fun removeFromStart(count: Int) {
         if (count <= 0) {
             return
         }
         if (count > size()) {
-            throw ArrayIndexOutOfBoundsException()
+            throw createIndexOutOfBoundsException()
         }
 
         var numOfElements = count
@@ -177,14 +180,14 @@
      * is less than or equals to 0.
      *
      * @param count Number of elements to remove.
-     * @throws ArrayIndexOutOfBoundsException if [count] is larger than [size]
+     * @throws [ArrayIndexOutOfBoundsException] if [count] is larger than [size]
      */
     public fun removeFromEnd(count: Int) {
         if (count <= 0) {
             return
         }
         if (count > size()) {
-            throw ArrayIndexOutOfBoundsException()
+            throw createIndexOutOfBoundsException()
         }
 
         var numOfElements = count
@@ -213,12 +216,12 @@
      * Get first element of the [CircularArray].
      *
      * @return The first element.
-     * @throws [ArrayIndexOutOfBoundsException] if [CircularArray] is empty.
+     * @throws [ArrayIndexOutOfBoundsException] if [CircularArray] is empty
      */
     public val first: E
         get() {
             if (head == tail) {
-                throw ArrayIndexOutOfBoundsException()
+                throw createIndexOutOfBoundsException()
             }
             return elements[head]!!
         }
@@ -227,12 +230,12 @@
      * Get last element of the [CircularArray].
      *
      * @return The last element.
-     * @throws [ArrayIndexOutOfBoundsException] if [CircularArray] is empty.
+     * @throws [ArrayIndexOutOfBoundsException] if [CircularArray] is empty
      */
     public val last: E
         get() {
             if (head == tail) {
-                throw ArrayIndexOutOfBoundsException()
+                throw createIndexOutOfBoundsException()
             }
             return elements[tail - 1 and capacityBitmask]!!
         }
@@ -242,11 +245,11 @@
      *
      * @param index The zero based element index in the [CircularArray].
      * @return The nth element.
-     * @throws [ArrayIndexOutOfBoundsException] if n < 0 or n >= size().
+     * @throws [ArrayIndexOutOfBoundsException] if n < 0 or n >= size()
      */
     public operator fun get(index: Int): E {
         if (index < 0 || index >= size()) {
-            throw ArrayIndexOutOfBoundsException()
+            throw createIndexOutOfBoundsException()
         }
         return elements[(head + index) and capacityBitmask]!!
     }
@@ -266,4 +269,4 @@
      * @return `true` if [size] is 0.
      */
     public fun isEmpty(): Boolean = head == tail
-}
+}
\ No newline at end of file
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/CollectionPlatformUtils.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/CollectionPlatformUtils.kt
new file mode 100644
index 0000000..f3cec22
--- /dev/null
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/CollectionPlatformUtils.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+/**
+ * Internal utils for handling implementation differences for the various targets of Collections.
+ */
+internal expect object CollectionPlatformUtils {
+
+    /**
+     * IndexOutOfBoundsException is the nearest kotlin common ancestor for the native and jvm
+     * specific implementations of ArrayIndexOutOfBoundsException.  Actuals should throw an
+     * exception specific to their target platform.
+     */
+    internal inline fun createIndexOutOfBoundsException(): IndexOutOfBoundsException
+}
diff --git a/collection/collection/src/jvmTest/java/androidx/collection/CircularArrayTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/CircularArrayTest.kt
similarity index 100%
rename from collection/collection/src/jvmTest/java/androidx/collection/CircularArrayTest.kt
rename to collection/collection/src/commonTest/kotlin/androidx/collection/CircularArrayTest.kt
diff --git a/collection/collection/src/jvmMain/kotlin/androidx/collection/CollectionPlatformUtils.kt b/collection/collection/src/jvmMain/kotlin/androidx/collection/CollectionPlatformUtils.kt
new file mode 100644
index 0000000..0233729
--- /dev/null
+++ b/collection/collection/src/jvmMain/kotlin/androidx/collection/CollectionPlatformUtils.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+/**
+ * JVM actual of internal utils for handling target differences in collection code.
+ */
+internal actual object CollectionPlatformUtils {
+
+    @Suppress("NOTHING_TO_INLINE")
+    internal actual inline fun createIndexOutOfBoundsException(): IndexOutOfBoundsException {
+        return ArrayIndexOutOfBoundsException()
+    }
+}
diff --git a/collection/collection/src/jvmTest/java/androidx/collection/LruCacheTest.java b/collection/collection/src/jvmTest/java/androidx/collection/LruCacheTest.java
deleted file mode 100644
index 6e14244..0000000
--- a/collection/collection/src/jvmTest/java/androidx/collection/LruCacheTest.java
+++ /dev/null
@@ -1,583 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.collection;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.fail;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-@RunWith(JUnit4.class)
-public class LruCacheTest {
-
-    private int mExpectedCreateCount;
-    private int mExpectedPutCount;
-    private int mExpectedHitCount;
-    private int mExpectedMissCount;
-    private int mExpectedEvictionCount;
-
-    @Test
-    public void testStatistics() {
-        LruCache<String, String> cache = new LruCache<String, String>(3);
-        assertStatistics(cache);
-        assertEquals(null, cache.put("a", "A"));
-        mExpectedPutCount++;
-        assertStatistics(cache);
-        assertHit(cache, "a", "A");
-        assertSnapshot(cache, "a", "A");
-        assertEquals(null, cache.put("b", "B"));
-        mExpectedPutCount++;
-        assertStatistics(cache);
-        assertHit(cache, "a", "A");
-        assertHit(cache, "b", "B");
-        assertSnapshot(cache, "a", "A", "b", "B");
-        assertEquals(null, cache.put("c", "C"));
-        mExpectedPutCount++;
-        assertStatistics(cache);
-        assertHit(cache, "a", "A");
-        assertHit(cache, "b", "B");
-        assertHit(cache, "c", "C");
-        assertSnapshot(cache, "a", "A", "b", "B", "c", "C");
-        assertEquals(null, cache.put("d", "D"));
-        mExpectedPutCount++;
-        mExpectedEvictionCount++; // a should have been evicted
-        assertStatistics(cache);
-        assertMiss(cache, "a");
-        assertHit(cache, "b", "B");
-        assertHit(cache, "c", "C");
-        assertHit(cache, "d", "D");
-        assertHit(cache, "b", "B");
-        assertHit(cache, "c", "C");
-        assertSnapshot(cache, "d", "D", "b", "B", "c", "C");
-        assertEquals(null, cache.put("e", "E"));
-        mExpectedPutCount++;
-        mExpectedEvictionCount++; // d should have been evicted
-        assertStatistics(cache);
-        assertMiss(cache, "d");
-        assertMiss(cache, "a");
-        assertHit(cache, "e", "E");
-        assertHit(cache, "b", "B");
-        assertHit(cache, "c", "C");
-        assertSnapshot(cache, "e", "E", "b", "B", "c", "C");
-    }
-
-    @Test
-    public void testStatisticsWithCreate() {
-        LruCache<String, String> cache = newCreatingCache();
-        assertStatistics(cache);
-        assertCreated(cache, "aa", "created-aa");
-        assertHit(cache, "aa", "created-aa");
-        assertSnapshot(cache, "aa", "created-aa");
-        assertCreated(cache, "bb", "created-bb");
-        assertMiss(cache, "c");
-        assertSnapshot(cache, "aa", "created-aa", "bb", "created-bb");
-        assertCreated(cache, "cc", "created-cc");
-        assertSnapshot(cache, "aa", "created-aa", "bb", "created-bb", "cc", "created-cc");
-        mExpectedEvictionCount++; // aa will be evicted
-        assertCreated(cache, "dd", "created-dd");
-        assertSnapshot(cache, "bb", "created-bb",  "cc", "created-cc", "dd", "created-dd");
-        mExpectedEvictionCount++; // bb will be evicted
-        assertCreated(cache, "aa", "created-aa");
-        assertSnapshot(cache, "cc", "created-cc", "dd", "created-dd", "aa", "created-aa");
-    }
-
-    @Test
-    public void testCreateOnCacheMiss() {
-        LruCache<String, String> cache = newCreatingCache();
-        String created = cache.get("aa");
-        assertEquals("created-aa", created);
-    }
-
-    @Test
-    public void testNoCreateOnCacheHit() {
-        LruCache<String, String> cache = newCreatingCache();
-        cache.put("aa", "put-aa");
-        assertEquals("put-aa", cache.get("aa"));
-    }
-
-    @Test
-    public void testConstructorDoesNotAllowZeroCacheSize() {
-        try {
-            new LruCache<String, String>(0);
-            fail();
-        } catch (IllegalArgumentException expected) {
-        }
-    }
-
-    @Test
-    public void testCannotPutNullKey() {
-        LruCache<String, String> cache = new LruCache<String, String>(3);
-        try {
-            cache.put(null, "A");
-            fail();
-        } catch (NullPointerException expected) {
-        }
-    }
-
-    @Test
-    public void testCannotPutNullValue() {
-        LruCache<String, String> cache = new LruCache<String, String>(3);
-        try {
-            cache.put("a", null);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-    }
-
-    @Test
-    public void testToString() {
-        LruCache<String, String> cache = new LruCache<String, String>(3);
-        assertEquals("LruCache[maxSize=3,hits=0,misses=0,hitRate=0%]", cache.toString());
-        cache.put("a", "A");
-        cache.put("b", "B");
-        cache.put("c", "C");
-        cache.put("d", "D");
-        cache.get("a"); // miss
-        cache.get("b"); // hit
-        cache.get("c"); // hit
-        cache.get("d"); // hit
-        cache.get("e"); // miss
-        assertEquals("LruCache[maxSize=3,hits=3,misses=2,hitRate=60%]", cache.toString());
-    }
-
-    @Test
-    public void testEvictionWithSingletonCache() {
-        LruCache<String, String> cache = new LruCache<String, String>(1);
-        cache.put("a", "A");
-        cache.put("b", "B");
-        assertSnapshot(cache, "b", "B");
-    }
-
-    @Test
-    public void testEntryEvictedWhenFull() {
-        List<String> log = new ArrayList<String>();
-        LruCache<String, String> cache = newRemovalLogCache(log);
-        cache.put("a", "A");
-        cache.put("b", "B");
-        cache.put("c", "C");
-        assertEquals(Collections.<String>emptyList(), log);
-        cache.put("d", "D");
-        assertEquals(Arrays.asList("a=A"), log);
-    }
-
-    /**
-     * Replacing the value for a key doesn't cause an eviction but it does bring
-     * the replaced entry to the front of the queue.
-     */
-    @Test
-    public void testPutCauseEviction() {
-        List<String> log = new ArrayList<String>();
-        LruCache<String, String> cache = newRemovalLogCache(log);
-        cache.put("a", "A");
-        cache.put("b", "B");
-        cache.put("c", "C");
-        cache.put("b", "B2");
-        assertEquals(Arrays.asList("b=B>B2"), log);
-        assertSnapshot(cache, "a", "A", "c", "C", "b", "B2");
-    }
-
-    @Test
-    public void testCustomSizesImpactsSize() {
-        LruCache<String, String> cache = new LruCache<String, String>(10) {
-            @Override protected int sizeOf(String key, String value) {
-                return key.length() + value.length();
-            }
-        };
-        assertEquals(0, cache.size());
-        cache.put("a", "AA");
-        assertEquals(3, cache.size());
-        cache.put("b", "BBBB");
-        assertEquals(8, cache.size());
-        cache.put("a", "");
-        assertEquals(6, cache.size());
-    }
-
-    @Test
-    public void testEvictionWithCustomSizes() {
-        LruCache<String, String> cache = new LruCache<String, String>(4) {
-            @Override protected int sizeOf(String key, String value) {
-                return value.length();
-            }
-        };
-        cache.put("a", "AAAA");
-        assertSnapshot(cache, "a", "AAAA");
-        cache.put("b", "BBBB"); // should evict a
-        assertSnapshot(cache, "b", "BBBB");
-        cache.put("c", "CC"); // should evict b
-        assertSnapshot(cache, "c", "CC");
-        cache.put("d", "DD");
-        assertSnapshot(cache, "c", "CC", "d", "DD");
-        cache.put("e", "E"); // should evict c
-        assertSnapshot(cache, "d", "DD", "e", "E");
-        cache.put("f", "F");
-        assertSnapshot(cache, "d", "DD", "e", "E", "f", "F");
-        cache.put("g", "G"); // should evict d
-        assertSnapshot(cache, "e", "E", "f", "F", "g", "G");
-        cache.put("h", "H");
-        assertSnapshot(cache, "e", "E", "f", "F", "g", "G", "h", "H");
-        cache.put("i", "III"); // should evict e, f, and g
-        assertSnapshot(cache, "h", "H", "i", "III");
-        cache.put("j", "JJJ"); // should evict h and i
-        assertSnapshot(cache, "j", "JJJ");
-    }
-
-    @Test
-    public void testEvictionThrowsWhenSizesAreInconsistent() {
-        LruCache<String, int[]> cache = new LruCache<String, int[]>(4) {
-            @Override protected int sizeOf(String key, int[] value) {
-                return value[0];
-            }
-        };
-        int[] a = { 4 };
-        cache.put("a", a);
-        // get the cache size out of sync
-        a[0] = 1;
-        assertEquals(4, cache.size());
-        // evict something
-        try {
-            cache.put("b", new int[] { 2 });
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-    }
-
-    @Test
-    public void testEvictionThrowsWhenSizesAreNegative() {
-        LruCache<String, String> cache = new LruCache<String, String>(4) {
-            @Override protected int sizeOf(String key, String value) {
-                return -1;
-            }
-        };
-        try {
-            cache.put("a", "A");
-            fail();
-        } catch (IllegalStateException expected) {
-        }
-    }
-
-    /**
-     * Naive caches evict at most one element at a time. This is problematic
-     * because evicting a small element may be insufficient to make room for a
-     * large element.
-     */
-    @Test
-    public void testDifferentElementSizes() {
-        LruCache<String, String> cache = new LruCache<String, String>(10) {
-            @Override protected int sizeOf(String key, String value) {
-                return value.length();
-            }
-        };
-        cache.put("a", "1");
-        cache.put("b", "12345678");
-        cache.put("c", "1");
-        assertSnapshot(cache, "a", "1", "b", "12345678", "c", "1");
-        cache.put("d", "12345678"); // should evict a and b
-        assertSnapshot(cache, "c", "1", "d", "12345678");
-        cache.put("e", "12345678"); // should evict c and d
-        assertSnapshot(cache, "e", "12345678");
-    }
-
-    @Test
-    public void testEvictAll() {
-        List<String> log = new ArrayList<String>();
-        LruCache<String, String> cache = newRemovalLogCache(log);
-        cache.put("a", "A");
-        cache.put("b", "B");
-        cache.put("c", "C");
-        cache.evictAll();
-        assertEquals(0, cache.size());
-        assertEquals(Arrays.asList("a=A", "b=B", "c=C"), log);
-    }
-
-    @Test
-    public void testEvictAllEvictsSizeZeroElements() {
-        LruCache<String, String> cache = new LruCache<String, String>(10) {
-            @Override protected int sizeOf(String key, String value) {
-                return 0;
-            }
-        };
-        cache.put("a", "A");
-        cache.put("b", "B");
-        cache.evictAll();
-        assertSnapshot(cache);
-    }
-
-    @Test
-    public void testRemoveWithCustomSizes() {
-        LruCache<String, String> cache = new LruCache<String, String>(10) {
-            @Override protected int sizeOf(String key, String value) {
-                return value.length();
-            }
-        };
-        cache.put("a", "123456");
-        cache.put("b", "1234");
-        cache.remove("a");
-        assertEquals(4, cache.size());
-    }
-
-    @Test
-    public void testRemoveAbsentElement() {
-        LruCache<String, String> cache = new LruCache<String, String>(10);
-        cache.put("a", "A");
-        cache.put("b", "B");
-        assertEquals(null, cache.remove("c"));
-        assertEquals(2, cache.size());
-    }
-
-    @Test
-    public void testRemoveNullThrows() {
-        LruCache<String, String> cache = new LruCache<String, String>(10);
-        try {
-            cache.remove(null);
-            fail();
-        } catch (NullPointerException expected) {
-        }
-    }
-
-    @Test
-    public void testRemoveCallsEntryRemoved() {
-        List<String> log = new ArrayList<String>();
-        LruCache<String, String> cache = newRemovalLogCache(log);
-        cache.put("a", "A");
-        cache.remove("a");
-        assertEquals(Arrays.asList("a=A>null"), log);
-    }
-
-    @Test
-    public void testPutCallsEntryRemoved() {
-        List<String> log = new ArrayList<String>();
-        LruCache<String, String> cache = newRemovalLogCache(log);
-        cache.put("a", "A");
-        cache.put("a", "A2");
-        assertEquals(Arrays.asList("a=A>A2"), log);
-    }
-
-    @Test
-    public void testEntryRemovedIsCalledWithoutSynchronization() {
-        LruCache<String, String> cache = new LruCache<String, String>(3) {
-            @Override protected void entryRemoved(
-                    boolean evicted, String key, String oldValue, String newValue) {
-                assertFalse(Thread.holdsLock(this));
-            }
-        };
-        cache.put("a", "A");
-        cache.put("a", "A2"); // replaced
-        cache.put("b", "B");
-        cache.put("c", "C");
-        cache.put("d", "D");  // single eviction
-        cache.remove("a");    // removed
-        cache.evictAll();     // multiple eviction
-    }
-
-    @Test
-    public void testCreateIsCalledWithoutSynchronization() {
-        LruCache<String, String> cache = new LruCache<String, String>(3) {
-            @Override protected String create(String key) {
-                assertFalse(Thread.holdsLock(this));
-                return null;
-            }
-        };
-        cache.get("a");
-    }
-
-    /**
-     * Test what happens when a value is added to the map while create is
-     * working. The map value should be returned by get(), and the created value
-     * should be released with entryRemoved().
-     */
-    @Test
-    public void testCreateWithConcurrentPut() {
-        final List<String> log = new ArrayList<String>();
-        LruCache<String, String> cache = new LruCache<String, String>(3) {
-            @Override protected String create(String key) {
-                put(key, "B");
-                return "A";
-            }
-            @Override protected void entryRemoved(
-                    boolean evicted, String key, String oldValue, String newValue) {
-                log.add(key + "=" + oldValue + ">" + newValue);
-            }
-        };
-        assertEquals("B", cache.get("a"));
-        assertEquals(Arrays.asList("a=A>B"), log);
-    }
-
-    /**
-     * Test what happens when two creates happen concurrently. The result from
-     * the first create to return is returned by both gets. The other created
-     * values should be released with entryRemove().
-     */
-    @Test
-    public void testCreateWithConcurrentCreate() {
-        final List<String> log = new ArrayList<String>();
-        LruCache<String, Integer> cache = new LruCache<String, Integer>(3) {
-            int mCallCount = 0;
-            @Override protected Integer create(String key) {
-                if (mCallCount++ == 0) {
-                    assertEquals(2, get(key).intValue());
-                    return 1;
-                } else {
-                    return 2;
-                }
-            }
-            @Override protected void entryRemoved(
-                    boolean evicted, String key, Integer oldValue, Integer newValue) {
-                log.add(key + "=" + oldValue + ">" + newValue);
-            }
-        };
-        assertEquals(2, cache.get("a").intValue());
-        assertEquals(Arrays.asList("a=1>2"), log);
-    }
-
-    /** Makes sure that LruCache operations are correctly synchronized to guarantee consistency. */
-    @Test
-    public void consistentMultithreadedAccess() {
-        class Tally {
-            int mNonNullValues;
-            int mNullValues;
-            int mValuesPut;
-            int mConflicts;
-            int mRemoved;
-        }
-
-        final Tally tally = new Tally();
-        final int rounds = 10000;
-        final String key = "key";
-        final int value = 42;
-        final LruCache<String, Integer> cache  = new LruCache<String, Integer>(1) {
-            @Override
-            protected Integer create(String key) {
-                return value;
-            }
-        };
-
-        Runnable r0 = new Runnable() {
-            @Override
-            public void run() {
-                for (int i = 0; i < rounds; i++) {
-                    if (cache.get(key) != null) {
-                        tally.mNonNullValues++;
-                    } else {
-                        tally.mNullValues++;
-                    }
-                }
-            }
-        };
-
-        Runnable r1 = new Runnable() {
-            @Override
-            public void run() {
-                for (int i = 0; i < rounds; i++) {
-                    if (i % 2 == 0) {
-                        if (cache.put(key, value) != null) {
-                            tally.mConflicts++;
-                        } else {
-                            tally.mValuesPut++;
-                        }
-                    } else {
-                        cache.remove(key);
-                        tally.mRemoved++;
-                    }
-                }
-            }
-        };
-
-
-        Thread t0 = new Thread(r0);
-        Thread t1 = new Thread(r1);
-
-        t0.start();
-        t1.start();
-        try {
-            t0.join();
-            t1.join();
-        } catch (InterruptedException e) {
-            fail();
-        }
-
-        assertEquals(rounds, tally.mNonNullValues);
-        assertEquals(0, tally.mNullValues);
-        assertEquals(rounds, tally.mValuesPut + tally.mConflicts + tally.mRemoved);
-    }
-
-    private LruCache<String, String> newCreatingCache() {
-        return new LruCache<String, String>(3) {
-            @Override protected String create(String key) {
-                return (key.length() > 1) ? ("created-" + key) : null;
-            }
-        };
-    }
-
-    private LruCache<String, String> newRemovalLogCache(final List<String> log) {
-        return new LruCache<String, String>(3) {
-            @Override protected void entryRemoved(
-                    boolean evicted, String key, String oldValue, String newValue) {
-                String message = evicted
-                        ? (key + "=" + oldValue)
-                        : (key + "=" + oldValue + ">" + newValue);
-                log.add(message);
-            }
-        };
-    }
-
-    private void assertHit(LruCache<String, String> cache, String key, String value) {
-        assertEquals(value, cache.get(key));
-        mExpectedHitCount++;
-        assertStatistics(cache);
-    }
-
-    private void assertMiss(LruCache<String, String> cache, String key) {
-        assertEquals(null, cache.get(key));
-        mExpectedMissCount++;
-        assertStatistics(cache);
-    }
-
-    private void assertCreated(LruCache<String, String> cache, String key, String value) {
-        assertEquals(value, cache.get(key));
-        mExpectedMissCount++;
-        mExpectedCreateCount++;
-        assertStatistics(cache);
-    }
-
-    private void assertStatistics(LruCache<?, ?> cache) {
-        assertEquals("create count", mExpectedCreateCount, cache.createCount());
-        assertEquals("put count", mExpectedPutCount, cache.putCount());
-        assertEquals("hit count", mExpectedHitCount, cache.hitCount());
-        assertEquals("miss count", mExpectedMissCount, cache.missCount());
-        assertEquals("eviction count", mExpectedEvictionCount, cache.evictionCount());
-    }
-
-    @SuppressWarnings("unchecked")
-    private <T> void assertSnapshot(LruCache<T, T> cache, T... keysAndValues) {
-        List<T> actualKeysAndValues = new ArrayList<T>();
-        for (Map.Entry<T, T> entry : cache.snapshot().entrySet()) {
-            actualKeysAndValues.add(entry.getKey());
-            actualKeysAndValues.add(entry.getValue());
-        }
-        // assert using lists because order is important for LRUs
-        assertEquals(Arrays.asList(keysAndValues), actualKeysAndValues);
-    }
-}
diff --git a/collection/collection/src/jvmTest/kotlin/androidx/collection/LruCacheJvmTest.kt b/collection/collection/src/jvmTest/kotlin/androidx/collection/LruCacheJvmTest.kt
new file mode 100644
index 0000000..8c0dc29
--- /dev/null
+++ b/collection/collection/src/jvmTest/kotlin/androidx/collection/LruCacheJvmTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.concurrent.thread
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+
+internal class LruCacheJvmTest {
+
+    @Test
+    fun testEntryRemovedIsCalledWithoutSynchronization() {
+        val cache =
+            object : LruCache<String, String>(3) {
+                override fun entryRemoved(
+                    evicted: Boolean,
+                    key: String,
+                    oldValue: String,
+                    newValue: String?
+                ) {
+                    assertFalse(Thread.holdsLock(this))
+                }
+            }
+        cache.put("a", "A")
+        cache.put("a", "A2") // replaced
+        cache.put("b", "B")
+        cache.put("c", "C")
+        cache.put("d", "D") // single eviction
+        cache.remove("a") // removed
+        cache.evictAll() // multiple eviction
+    }
+
+    /** Makes sure that LruCache operations are correctly synchronized to guarantee consistency.  */
+    @Test
+    fun consistentMultithreadedAccess() {
+        var nonNullValues = 0
+        var nullValues = 0
+        var valuesPut = 0
+        var conflicts = 0
+        var removed = 0
+
+        val rounds = 10000
+        val key = "key"
+        val value = 42
+        val cache =
+            object : LruCache<String, Int>(1) {
+                override fun create(key: String): Int = value
+            }
+
+        val t0 =
+            thread {
+                repeat(rounds) {
+                    if (cache[key] != null) {
+                        nonNullValues++
+                    } else {
+                        nullValues++
+                    }
+                }
+            }
+
+        val t1 =
+            thread {
+                repeat(rounds) { i ->
+                    if (i % 2 == 0) {
+                        if (cache.put(key, value) != null) {
+                            conflicts++
+                        } else {
+                            valuesPut++
+                        }
+                    } else {
+                        cache.remove(key)
+                        removed++
+                    }
+                }
+            }
+
+        t0.join()
+        t1.join()
+
+        assertEquals(rounds, nonNullValues)
+        assertEquals(0, nullValues)
+        assertEquals(rounds, valuesPut + conflicts + removed)
+    }
+}
diff --git a/collection/collection/src/jvmTest/kotlin/androidx/collection/LruCacheTest.kt b/collection/collection/src/jvmTest/kotlin/androidx/collection/LruCacheTest.kt
new file mode 100644
index 0000000..98d7ee6
--- /dev/null
+++ b/collection/collection/src/jvmTest/kotlin/androidx/collection/LruCacheTest.kt
@@ -0,0 +1,437 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.collection
+
+import kotlin.test.Test
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNull
+
+internal class LruCacheTest {
+
+    private var expectedCreateCount = 0
+    private var expectedPutCount = 0
+    private var expectedHitCount = 0
+    private var expectedMissCount = 0
+    private var expectedEvictionCount = 0
+
+    @Test
+    fun testStatistics() {
+        val cache = LruCache<String, String>(3)
+        assertStatistics(cache)
+        assertNull(cache.put("a", "A"))
+        expectedPutCount++
+        assertStatistics(cache)
+        assertHit(cache, "a", "A")
+        assertSnapshot(cache, "a", "A")
+        assertNull(cache.put("b", "B"))
+        expectedPutCount++
+        assertStatistics(cache)
+        assertHit(cache, "a", "A")
+        assertHit(cache, "b", "B")
+        assertSnapshot(cache, "a", "A", "b", "B")
+        assertNull(cache.put("c", "C"))
+        expectedPutCount++
+        assertStatistics(cache)
+        assertHit(cache, "a", "A")
+        assertHit(cache, "b", "B")
+        assertHit(cache, "c", "C")
+        assertSnapshot(cache, "a", "A", "b", "B", "c", "C")
+        assertNull(cache.put("d", "D"))
+        expectedPutCount++
+        expectedEvictionCount++ // a should have been evicted
+        assertStatistics(cache)
+        assertMiss(cache, "a")
+        assertHit(cache, "b", "B")
+        assertHit(cache, "c", "C")
+        assertHit(cache, "d", "D")
+        assertHit(cache, "b", "B")
+        assertHit(cache, "c", "C")
+        assertSnapshot(cache, "d", "D", "b", "B", "c", "C")
+        assertNull(cache.put("e", "E"))
+        expectedPutCount++
+        expectedEvictionCount++ // d should have been evicted
+        assertStatistics(cache)
+        assertMiss(cache, "d")
+        assertMiss(cache, "a")
+        assertHit(cache, "e", "E")
+        assertHit(cache, "b", "B")
+        assertHit(cache, "c", "C")
+        assertSnapshot(cache, "e", "E", "b", "B", "c", "C")
+    }
+
+    @Test
+    fun testStatisticsWithCreate() {
+        val cache = newCreatingCache()
+        assertStatistics(cache)
+        assertCreated(cache, "aa", "created-aa")
+        assertHit(cache, "aa", "created-aa")
+        assertSnapshot(cache, "aa", "created-aa")
+        assertCreated(cache, "bb", "created-bb")
+        assertMiss(cache, "c")
+        assertSnapshot(cache, "aa", "created-aa", "bb", "created-bb")
+        assertCreated(cache, "cc", "created-cc")
+        assertSnapshot(cache, "aa", "created-aa", "bb", "created-bb", "cc", "created-cc")
+        expectedEvictionCount++ // aa will be evicted
+        assertCreated(cache, "dd", "created-dd")
+        assertSnapshot(cache, "bb", "created-bb", "cc", "created-cc", "dd", "created-dd")
+        expectedEvictionCount++ // bb will be evicted
+        assertCreated(cache, "aa", "created-aa")
+        assertSnapshot(cache, "cc", "created-cc", "dd", "created-dd", "aa", "created-aa")
+    }
+
+    @Test
+    fun testCreateOnCacheMiss() {
+        val cache = newCreatingCache()
+        val created = cache["aa"]
+        assertEquals("created-aa", created)
+    }
+
+    @Test
+    fun testNoCreateOnCacheHit() {
+        val cache = newCreatingCache()
+        cache.put("aa", "put-aa")
+        assertEquals("put-aa", cache["aa"])
+    }
+
+    @Test
+    fun testConstructorDoesNotAllowZeroCacheSize() {
+        assertFailsWith<IllegalArgumentException> {
+            LruCache<String, String>(0)
+        }
+    }
+
+    @Test
+    fun testToString() {
+        val cache = LruCache<String, String>(3)
+        assertEquals("LruCache[maxSize=3,hits=0,misses=0,hitRate=0%]", cache.toString())
+        cache.put("a", "A")
+        cache.put("b", "B")
+        cache.put("c", "C")
+        cache.put("d", "D")
+        cache["a"] // miss
+        cache["b"] // hit
+        cache["c"] // hit
+        cache["d"] // hit
+        cache["e"] // miss
+        assertEquals("LruCache[maxSize=3,hits=3,misses=2,hitRate=60%]", cache.toString())
+    }
+
+    @Test
+    fun testEvictionWithSingletonCache() {
+        val cache = LruCache<String, String>(1)
+        cache.put("a", "A")
+        cache.put("b", "B")
+        assertSnapshot(cache, "b", "B")
+    }
+
+    @Test
+    fun testEntryEvictedWhenFull() {
+        val log = ArrayList<String>()
+        val cache = newRemovalLogCache(log)
+        cache.put("a", "A")
+        cache.put("b", "B")
+        cache.put("c", "C")
+        assertContentEquals(emptyList(), log)
+        cache.put("d", "D")
+        assertContentEquals(listOf("a=A"), log)
+    }
+
+    /**
+     * Replacing the value for a key doesn't cause an eviction but it does bring
+     * the replaced entry to the front of the queue.
+     */
+    @Test
+    fun testPutCauseEviction() {
+        val log = ArrayList<String>()
+        val cache = newRemovalLogCache(log)
+        cache.put("a", "A")
+        cache.put("b", "B")
+        cache.put("c", "C")
+        cache.put("b", "B2")
+        assertContentEquals(listOf("b=B>B2"), log)
+        assertSnapshot(cache, "a", "A", "c", "C", "b", "B2")
+    }
+
+    @Test
+    fun testCustomSizesImpactsSize() {
+        val cache =
+            object : LruCache<String, String>(10) {
+                override fun sizeOf(key: String, value: String): Int =
+                    key.length + value.length
+            }
+        assertEquals(0, cache.size())
+        cache.put("a", "AA")
+        assertEquals(3, cache.size())
+        cache.put("b", "BBBB")
+        assertEquals(8, cache.size())
+        cache.put("a", "")
+        assertEquals(6, cache.size())
+    }
+
+    @Test
+    fun testEvictionWithCustomSizes() {
+        val cache =
+            object : LruCache<String, String>(4) {
+                override fun sizeOf(key: String, value: String): Int = value.length
+            }
+        cache.put("a", "AAAA")
+        assertSnapshot(cache, "a", "AAAA")
+        cache.put("b", "BBBB") // should evict a
+        assertSnapshot(cache, "b", "BBBB")
+        cache.put("c", "CC") // should evict b
+        assertSnapshot(cache, "c", "CC")
+        cache.put("d", "DD")
+        assertSnapshot(cache, "c", "CC", "d", "DD")
+        cache.put("e", "E") // should evict c
+        assertSnapshot(cache, "d", "DD", "e", "E")
+        cache.put("f", "F")
+        assertSnapshot(cache, "d", "DD", "e", "E", "f", "F")
+        cache.put("g", "G") // should evict d
+        assertSnapshot(cache, "e", "E", "f", "F", "g", "G")
+        cache.put("h", "H")
+        assertSnapshot(cache, "e", "E", "f", "F", "g", "G", "h", "H")
+        cache.put("i", "III") // should evict e, f, and g
+        assertSnapshot(cache, "h", "H", "i", "III")
+        cache.put("j", "JJJ") // should evict h and i
+        assertSnapshot(cache, "j", "JJJ")
+    }
+
+    @Test
+    fun testEvictionThrowsWhenSizesAreInconsistent() {
+        val cache = object : LruCache<String, IntArray>(4) {
+            override fun sizeOf(key: String, value: IntArray): Int = value[0]
+        }
+        val a = intArrayOf(4)
+        cache.put("a", a)
+        // get the cache size out of sync
+        a[0] = 1
+        assertEquals(4, cache.size())
+        // evict something
+        assertFailsWith<IllegalStateException> {
+            cache.put("b", intArrayOf(2))
+        }
+    }
+
+    @Test
+    fun testEvictionThrowsWhenSizesAreNegative() {
+        val cache =
+            object : LruCache<String, String>(4) {
+                override fun sizeOf(key: String, value: String): Int = -1
+            }
+        assertFailsWith<IllegalStateException> {
+            cache.put("a", "A")
+        }
+    }
+
+    /**
+     * Naive caches evict at most one element at a time. This is problematic
+     * because evicting a small element may be insufficient to make room for a
+     * large element.
+     */
+    @Test
+    fun testDifferentElementSizes() {
+        val cache =
+            object : LruCache<String, String>(10) {
+                override fun sizeOf(key: String, value: String): Int = value.length
+            }
+        cache.put("a", "1")
+        cache.put("b", "12345678")
+        cache.put("c", "1")
+        assertSnapshot(cache, "a", "1", "b", "12345678", "c", "1")
+        cache.put("d", "12345678") // should evict a and b
+        assertSnapshot(cache, "c", "1", "d", "12345678")
+        cache.put("e", "12345678") // should evict c and d
+        assertSnapshot(cache, "e", "12345678")
+    }
+
+    @Test
+    fun testEvictAll() {
+        val log = ArrayList<String>()
+        val cache = newRemovalLogCache(log)
+        cache.put("a", "A")
+        cache.put("b", "B")
+        cache.put("c", "C")
+        cache.evictAll()
+        assertEquals(0, cache.size())
+        assertContentEquals(listOf("a=A", "b=B", "c=C"), log)
+    }
+
+    @Test
+    fun testEvictAllEvictsSizeZeroElements() {
+        val cache =
+            object : LruCache<String, String>(10) {
+                override fun sizeOf(key: String, value: String): Int = 0
+            }
+        cache.put("a", "A")
+        cache.put("b", "B")
+        cache.evictAll()
+        assertSnapshot(cache)
+    }
+
+    @Test
+    fun testRemoveWithCustomSizes() {
+        val cache =
+            object : LruCache<String, String>(10) {
+                override fun sizeOf(key: String, value: String): Int = value.length
+            }
+        cache.put("a", "123456")
+        cache.put("b", "1234")
+        cache.remove("a")
+        assertEquals(4, cache.size())
+    }
+
+    @Test
+    fun testRemoveAbsentElement() {
+        val cache = LruCache<String, String>(10)
+        cache.put("a", "A")
+        cache.put("b", "B")
+        assertNull(cache.remove("c"))
+        assertEquals(2, cache.size())
+    }
+
+    @Test
+    fun testRemoveCallsEntryRemoved() {
+        val log = ArrayList<String>()
+        val cache = newRemovalLogCache(log)
+        cache.put("a", "A")
+        cache.remove("a")
+        assertContentEquals(listOf("a=A>null"), log)
+    }
+
+    @Test
+    fun testPutCallsEntryRemoved() {
+        val log = ArrayList<String>()
+        val cache = newRemovalLogCache(log)
+        cache.put("a", "A")
+        cache.put("a", "A2")
+        assertContentEquals(listOf("a=A>A2"), log)
+    }
+
+    /**
+     * Test what happens when a value is added to the map while create is
+     * working. The map value should be returned by get(), and the created value
+     * should be released with entryRemoved().
+     */
+    @Test
+    fun testCreateWithConcurrentPut() {
+        val log = java.util.ArrayList<String>()
+        val cache =
+            object : LruCache<String, String>(3) {
+                override fun create(key: String): String {
+                    put(key, "B")
+                    return "A"
+                }
+
+                override fun entryRemoved(
+                    evicted: Boolean,
+                    key: String,
+                    oldValue: String,
+                    newValue: String?
+                ) {
+                    log.add("$key=$oldValue>$newValue")
+                }
+            }
+        assertEquals("B", cache["a"])
+        assertContentEquals(listOf("a=A>B"), log)
+    }
+
+    /**
+     * Test what happens when two creates happen concurrently. The result from
+     * the first create to return is returned by both gets. The other created
+     * values should be released with entryRemove().
+     */
+    @Test
+    fun testCreateWithConcurrentCreate() {
+        val log = ArrayList<String>()
+        val cache =
+            object : LruCache<String, Int>(3) {
+                var mCallCount = 0
+
+                override fun create(key: String): Int =
+                    if (mCallCount++ == 0) {
+                        assertEquals(2, get(key))
+                        1
+                    } else {
+                        2
+                    }
+
+                override fun entryRemoved(
+                    evicted: Boolean,
+                    key: String,
+                    oldValue: Int,
+                    newValue: Int?
+                ) {
+                    log.add("$key=$oldValue>$newValue")
+                }
+            }
+        assertEquals(2, cache["a"])
+        assertContentEquals(listOf("a=1>2"), log)
+    }
+
+    private fun newCreatingCache(): LruCache<String, String> =
+        object : LruCache<String, String>(3) {
+            override fun create(key: String): String? =
+                if (key.length > 1) "created-$key" else null
+        }
+
+    private fun newRemovalLogCache(log: MutableList<String>): LruCache<String, String> =
+        object : LruCache<String, String>(3) {
+            override fun entryRemoved(
+                evicted: Boolean,
+                key: String,
+                oldValue: String,
+                newValue: String?
+            ) {
+                log += if (evicted) "$key=$oldValue" else "$key=$oldValue>$newValue"
+            }
+        }
+
+    private fun assertHit(cache: LruCache<String, String>, key: String, value: String) {
+        assertEquals(value, cache[key])
+        expectedHitCount++
+        assertStatistics(cache)
+    }
+
+    private fun assertMiss(cache: LruCache<String, String>, key: String) {
+        assertEquals(null, cache[key])
+        expectedMissCount++
+        assertStatistics(cache)
+    }
+
+    private fun assertCreated(cache: LruCache<String, String>, key: String, value: String) {
+        assertEquals(value, cache[key])
+        expectedMissCount++
+        expectedCreateCount++
+        assertStatistics(cache)
+    }
+
+    private fun assertStatistics(cache: LruCache<*, *>) {
+        assertEquals(expectedCreateCount, cache.createCount(), "create count")
+        assertEquals(expectedPutCount, cache.putCount(), "put count")
+        assertEquals(expectedHitCount, cache.hitCount(), "hit count")
+        assertEquals(expectedMissCount, cache.missCount(), "miss count")
+        assertEquals(expectedEvictionCount, cache.evictionCount(), "eviction count")
+    }
+
+    private fun <T : Any> assertSnapshot(cache: LruCache<T, T>, vararg keysAndValues: T) {
+        val actualKeysAndValues = cache.snapshot().flatMap { (key, value) -> listOf(key, value) }
+        // assert using lists because order is important for LRUs
+        assertContentEquals(keysAndValues.asList(), actualKeysAndValues)
+    }
+}
diff --git a/health/health-data-client/src/main/java/androidx/health/platform/client/error/HealthDataException.kt b/collection/collection/src/nativeMain/kotlin/androidx/collection/CollectionPlatformUtils.kt
similarity index 60%
copy from health/health-data-client/src/main/java/androidx/health/platform/client/error/HealthDataException.kt
copy to collection/collection/src/nativeMain/kotlin/androidx/collection/CollectionPlatformUtils.kt
index e2d1362..678d982 100644
--- a/health/health-data-client/src/main/java/androidx/health/platform/client/error/HealthDataException.kt
+++ b/collection/collection/src/nativeMain/kotlin/androidx/collection/CollectionPlatformUtils.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,11 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.health.platform.client.error
+package androidx.collection
 
 /**
- * Exception thrown by Health Platform. Contains one of [ErrorCode]s and message with details on the
- * error.
+ * Native actual of internal utils for handling target differences in collection code.
  */
-open class HealthDataException(val errorCode: Int, errorMessage: String? = "") :
-    Exception("$errorCode: $errorMessage")
+internal actual object CollectionPlatformUtils {
+
+    internal actual inline fun createIndexOutOfBoundsException(): IndexOutOfBoundsException {
+        return ArrayIndexOutOfBoundsException()
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
index 143cc90..dd8c203 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
@@ -21,6 +21,8 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.relocation.BringIntoViewResponder
+import androidx.compose.foundation.relocation.bringIntoViewResponder
 import androidx.compose.foundation.lazy.layout.ModifierLocalPinnableParent
 import androidx.compose.foundation.lazy.layout.PinnableParent
 import androidx.compose.foundation.lazy.layout.PinnableParent.PinnedItemsHandle
@@ -33,6 +35,9 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.modifier.modifierLocalProvider
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
@@ -57,6 +62,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 class FocusableTest {
@@ -77,7 +83,7 @@
     }
 
     @Test
-    fun focusableTest_defaultSemantics() {
+    fun focusable_defaultSemantics() {
         rule.setContent {
             Box {
                 BasicText(
@@ -93,7 +99,7 @@
     }
 
     @Test
-    fun focusableTest_disabledSemantics() {
+    fun focusable_disabledSemantics() {
         rule.setContent {
             Box {
                 BasicText(
@@ -107,9 +113,8 @@
             .assert(isNotFocusable())
     }
 
-    @ExperimentalComposeUiApi
     @Test
-    fun focusableTest_focusAcquire() {
+    fun focusable_focusAcquire() {
         val (focusRequester, otherFocusRequester) = FocusRequester.createRefs()
         rule.setContent {
             Box {
@@ -147,9 +152,8 @@
             .assertIsNotFocused()
     }
 
-    @ExperimentalComposeUiApi
     @Test
-    fun focusableTest_interactionSource() {
+    fun focusable_interactionSource() {
         val interactionSource = MutableInteractionSource()
         val (focusRequester, otherFocusRequester) = FocusRequester.createRefs()
 
@@ -208,7 +212,7 @@
     }
 
     @Test
-    fun focusableTest_interactionSource_resetWhenDisposed() {
+    fun focusable_interactionSource_resetWhenDisposed() {
         val interactionSource = MutableInteractionSource()
         val focusRequester = FocusRequester()
         var emitFocusableText by mutableStateOf(true)
@@ -298,16 +302,48 @@
     }
 
     @Test
-    fun focusableText_testInspectorValue() {
+    fun focusable_inspectorValue() {
+        val modifier = Modifier.focusable() as InspectableValue
+        assertThat(modifier.nameFallback).isEqualTo("focusable")
+        assertThat(modifier.valueOverride).isNull()
+        assertThat(modifier.inspectableElements.map { it.name }.asIterable())
+            .containsExactly(
+                "enabled",
+                "interactionSource"
+            )
+    }
+
+    @Test
+    fun focusable_requestsBringIntoView_whenFocused() {
+        val requestedRects = mutableListOf<Rect>()
+        val bringIntoViewResponder = object : BringIntoViewResponder {
+            override fun calculateRectForParent(localRect: Rect): Rect = localRect
+            override suspend fun bringChildIntoView(localRect: Rect) {
+                requestedRects += localRect
+            }
+        }
+        val focusRequester = FocusRequester()
+
         rule.setContent {
-            val modifier = Modifier.focusable() as InspectableValue
-            assertThat(modifier.nameFallback).isEqualTo("focusable")
-            assertThat(modifier.valueOverride).isNull()
-            assertThat(modifier.inspectableElements.map { it.name }.asIterable())
-                .containsExactly(
-                    "enabled",
-                    "interactionSource"
+            with(rule.density) {
+                Box(
+                    Modifier
+                        .bringIntoViewResponder(bringIntoViewResponder)
+                        .focusRequester(focusRequester)
+                        .focusable()
+                        // Needs a non-zero size.
+                        .size(1f.toDp())
                 )
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(requestedRects).isEmpty()
+            focusRequester.requestFocus()
+        }
+
+        rule.runOnIdle {
+            assertThat(requestedRects).containsExactly(Rect(Offset.Zero, Size(1f, 1f)))
         }
     }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt
index 5d682d7..af5d456 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt
@@ -25,7 +25,6 @@
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
@@ -46,7 +45,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
+@OptIn(ExperimentalFoundationApi::class)
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 class BringIntoViewRequesterViewIntegrationTest {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
index 70eba2f..5e07173 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
@@ -21,7 +21,6 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.size
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
@@ -41,7 +40,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
+@OptIn(ExperimentalFoundationApi::class)
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class BringIntoViewResponderTest {
@@ -49,13 +48,13 @@
     @get:Rule
     val rule = createAndroidComposeRule<TestActivity>()
 
-    fun Float.toDp(): Dp = with(rule.density) { this@toDp.toDp() }
+    private fun Float.toDp(): Dp = with(rule.density) { this@toDp.toDp() }
 
     @Test
     fun zeroSizedItem_zeroSizedParent_bringIntoView() {
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
-        lateinit var requestedRect: Rect
+        var requestedRect: Rect? = null
         rule.setContent {
             Box(
                 Modifier
@@ -65,7 +64,9 @@
         }
 
         // Act.
-        runBlocking { bringIntoViewRequester.bringIntoView() }
+        runBlocking {
+            bringIntoViewRequester.bringIntoView()
+        }
 
         // Assert.
         rule.runOnIdle {
@@ -131,8 +132,8 @@
                 Modifier
                     .size(1f.toDp())
                     .fakeScrollable { requestedRect = it }
-                    .size(20f.toDp(), 10f.toDp())
                     .bringIntoViewRequester(bringIntoViewRequester)
+                    .size(20f.toDp(), 10f.toDp())
             )
         }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
index dba0121..0b02623 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
@@ -90,6 +90,7 @@
         keyboardHelper.waitForKeyboardVisibility(visible = true)
     }
 
+    @FlakyTest(bugId = 229247491)
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
     @Test
     fun keyboardHiddenWhenFocusIsLost() {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/FocusedBounds.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/FocusedBounds.kt
index 2e23bf6..1f25b23 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/FocusedBounds.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/FocusedBounds.kt
@@ -17,7 +17,6 @@
 package androidx.compose.foundation
 
 import androidx.compose.runtime.remember
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.layout.LayoutCoordinates
@@ -45,7 +44,6 @@
  * Note that there may be some cases where the focused bounds change but the callback is _not_
  * invoked, but the last [LayoutCoordinates] will always return the most up-to-date bounds.
  */
-@OptIn(ExperimentalComposeUiApi::class)
 @ExperimentalFoundationApi
 fun Modifier.onFocusedBoundsChanged(onPositioned: (LayoutCoordinates?) -> Unit): Modifier =
     composed(
@@ -57,7 +55,6 @@
         remember(onPositioned) { FocusedBoundsObserverModifier(onPositioned) }
     }
 
-@OptIn(ExperimentalFoundationApi::class)
 private class FocusedBoundsObserverModifier(
     private val handler: (LayoutCoordinates?) -> Unit
 ) : ModifierLocalConsumer,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
index 35aa72c..57efd71 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
@@ -19,7 +19,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.OnGloballyPositionedModifier
+import androidx.compose.ui.layout.OnPlacedModifier
 import androidx.compose.ui.modifier.ModifierLocalConsumer
 import androidx.compose.ui.modifier.ModifierLocalReadScope
 import androidx.compose.ui.modifier.modifierLocalOf
@@ -69,7 +69,7 @@
 internal abstract class BringIntoViewChildModifier(
     private val defaultParent: BringIntoViewParent
 ) : ModifierLocalConsumer,
-    OnGloballyPositionedModifier {
+    OnPlacedModifier {
 
     private var localParent: BringIntoViewParent? = null
 
@@ -87,7 +87,7 @@
         }
     }
 
-    override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
+    override fun onPlaced(coordinates: LayoutCoordinates) {
         layoutCoordinates = coordinates
     }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMagnifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMagnifier.kt
index 9beb190..531f018 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMagnifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionMagnifier.kt
@@ -28,7 +28,6 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Offset
@@ -61,7 +60,6 @@
  * The text magnifier follows horizontal dragging exactly, but is vertically clamped to the current
  * line, so when it changes lines we animate it.
  */
-@OptIn(ExperimentalComposeUiApi::class)
 @Suppress("ModifierInspectorInfo")
 internal fun Modifier.animatedSelectionMagnifier(
     magnifierCenter: () -> Offset,
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index a211c10..735f25a 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -237,6 +237,8 @@
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors filledIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors filledTonalIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors filledTonalIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors iconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors iconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedIconButtonBorder(boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonBorder(boolean enabled, boolean checked);
@@ -245,8 +247,8 @@
   }
 
   public final class IconButtonKt {
-    method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.IconButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.IconToggleButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
   public final class IconKt {
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 78c1a14..98e9c45 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -313,6 +313,8 @@
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors filledIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors filledTonalIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors filledTonalIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors iconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors iconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedIconButtonBorder(boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonBorder(boolean enabled, boolean checked);
@@ -325,8 +327,8 @@
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilledIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconToggleButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilledTonalIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilledTonalIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.IconToggleButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.IconButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.IconToggleButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void OutlinedIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.material3.IconButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void OutlinedIconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.BorderStroke? border, optional androidx.compose.material3.IconToggleButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index a211c10..735f25a 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -237,6 +237,8 @@
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors filledIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors filledTonalIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors filledTonalIconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors iconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
+    method @androidx.compose.runtime.Composable public androidx.compose.material3.IconToggleButtonColors iconToggleButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor, optional long checkedContainerColor, optional long checkedContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke outlinedIconButtonBorder(boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.material3.IconButtonColors outlinedIconButtonColors(optional long containerColor, optional long contentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke? outlinedIconToggleButtonBorder(boolean enabled, boolean checked);
@@ -245,8 +247,8 @@
   }
 
   public final class IconButtonKt {
-    method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.IconButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void IconToggleButton(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.IconToggleButtonColors colors, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
   public final class IconKt {
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index 05448b4..5239032 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -96,10 +96,10 @@
 import androidx.compose.material3.samples.TextButtonSample
 import androidx.compose.material3.samples.TextFieldSample
 import androidx.compose.material3.samples.TextFieldWithErrorState
-import androidx.compose.material3.samples.TextFieldWithHelperMessage
 import androidx.compose.material3.samples.TextFieldWithHideKeyboardOnImeAction
 import androidx.compose.material3.samples.TextFieldWithIcons
 import androidx.compose.material3.samples.TextFieldWithPlaceholder
+import androidx.compose.material3.samples.TextFieldWithSupportingText
 import androidx.compose.material3.samples.TextTabs
 import androidx.compose.material3.samples.TriStateCheckboxSample
 import androidx.compose.runtime.Composable
@@ -684,11 +684,11 @@
         TextFieldWithErrorState()
     },
     Example(
-        name = ::TextFieldWithHelperMessage.name,
+        name = ::TextFieldWithSupportingText.name,
         description = TextFieldsExampleDescription,
         sourceUrl = TextFieldsExampleSourceUrl
     ) {
-        TextFieldWithHelperMessage()
+        TextFieldWithSupportingText()
     },
     Example(
         name = ::PasswordTextField.name,
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt
index 44ae71a..b9b8a42 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TabSamples.kt
@@ -61,7 +61,7 @@
 @Composable
 fun TextTabs() {
     var state by remember { mutableStateOf(0) }
-    val titles = listOf("TAB 1", "TAB 2", "TAB 3 WITH LOTS OF TEXT")
+    val titles = listOf("Tab 1", "Tab 2", "Tab 3 with lots of text")
     Column {
         TabRow(selectedTabIndex = state) {
             titles.forEachIndexed { index, title ->
@@ -106,9 +106,9 @@
 fun TextAndIconTabs() {
     var state by remember { mutableStateOf(0) }
     val titlesAndIcons = listOf(
-        "TAB 1" to Icons.Filled.Favorite,
-        "TAB 2" to Icons.Filled.Favorite,
-        "TAB 3 WITH LOTS OF TEXT" to Icons.Filled.Favorite
+        "Tab 1" to Icons.Filled.Favorite,
+        "Tab 2" to Icons.Filled.Favorite,
+        "Tab 3 with lots of text" to Icons.Filled.Favorite
     )
     Column {
         TabRow(selectedTabIndex = state) {
@@ -133,9 +133,9 @@
 fun LeadingIconTabs() {
     var state by remember { mutableStateOf(0) }
     val titlesAndIcons = listOf(
-        "TAB" to Icons.Filled.Favorite,
-        "TAB & ICON" to Icons.Filled.Favorite,
-        "TAB 3 WITH LOTS OF TEXT" to Icons.Filled.Favorite
+        "Tab" to Icons.Filled.Favorite,
+        "Tab & icon" to Icons.Filled.Favorite,
+        "Tab 3 with lots of text" to Icons.Filled.Favorite
     )
     Column {
         TabRow(selectedTabIndex = state) {
@@ -160,16 +160,16 @@
 fun ScrollingTextTabs() {
     var state by remember { mutableStateOf(0) }
     val titles = listOf(
-        "TAB 1",
-        "TAB 2",
-        "TAB 3 WITH LOTS OF TEXT",
-        "TAB 4",
-        "TAB 5",
-        "TAB 6 WITH LOTS OF TEXT",
-        "TAB 7",
-        "TAB 8",
-        "TAB 9 WITH LOTS OF TEXT",
-        "TAB 10"
+        "Tab 1",
+        "Tab 2",
+        "Tab 3 with lots of text",
+        "Tab 4",
+        "Tab 5",
+        "Tab 6 with lots of text",
+        "Tab 7",
+        "Tab 8",
+        "Tab 9 with lots of text",
+        "Tab 10"
     )
     Column {
         ScrollableTabRow(selectedTabIndex = state) {
@@ -193,7 +193,7 @@
 @Composable
 fun FancyTabs() {
     var state by remember { mutableStateOf(0) }
-    val titles = listOf("TAB 1", "TAB 2", "TAB 3")
+    val titles = listOf("Tab 1", "Tab 2", "Tab 3")
     Column {
         TabRow(selectedTabIndex = state) {
             titles.forEachIndexed { index, title ->
@@ -212,7 +212,7 @@
 @Composable
 fun FancyIndicatorTabs() {
     var state by remember { mutableStateOf(0) }
-    val titles = listOf("TAB 1", "TAB 2", "TAB 3")
+    val titles = listOf("Tab 1", "Tab 2", "Tab 3")
 
     // Reuse the default offset animation modifier, but use our own indicator
     val indicator = @Composable { tabPositions: List<TabPosition> ->
@@ -244,7 +244,7 @@
 @Composable
 fun FancyIndicatorContainerTabs() {
     var state by remember { mutableStateOf(0) }
-    val titles = listOf("TAB 1", "TAB 2", "TAB 3")
+    val titles = listOf("Tab 1", "Tab 2", "Tab 3")
 
     val indicator = @Composable { tabPositions: List<TabPosition> ->
         FancyAnimatedIndicator(tabPositions = tabPositions, selectedTabIndex = state)
@@ -275,16 +275,16 @@
 fun ScrollingFancyIndicatorContainerTabs() {
     var state by remember { mutableStateOf(0) }
     val titles = listOf(
-        "TAB 1",
-        "TAB 2",
-        "TAB 3 WITH LOTS OF TEXT",
-        "TAB 4",
-        "TAB 5",
-        "TAB 6 WITH LOTS OF TEXT",
-        "TAB 7",
-        "TAB 8",
-        "TAB 9 WITH LOTS OF TEXT",
-        "TAB 10"
+        "Tab 1",
+        "Tab 2",
+        "Tab 3 with lots of text",
+        "Tab 4",
+        "Tab 5",
+        "Tab 6 with lots of text",
+        "Tab 7",
+        "Tab 8",
+        "Tab 9 with lots of text",
+        "Tab 10"
     )
     val indicator = @Composable { tabPositions: List<TabPosition> ->
         FancyAnimatedIndicator(tabPositions = tabPositions, selectedTabIndex = state)
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt
index 4bd25b0..ec9f8e4 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/TextFieldSamples.kt
@@ -48,6 +48,7 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.vector.ImageVector
@@ -117,33 +118,43 @@
 @Sampled
 @Composable
 fun TextFieldWithErrorState() {
+    val errorMessage = "Email format is invalid"
     var text by rememberSaveable { mutableStateOf("") }
     var isError by rememberSaveable { mutableStateOf(false) }
 
     fun validate(text: String) {
-        isError = text.count() < 5
+        isError = !text.contains('@')
     }
 
-    TextField(
-        value = text,
-        onValueChange = {
-            text = it
-            isError = false
-        },
-        singleLine = true,
-        label = { Text(if (isError) "Email*" else "Email") },
-        isError = isError,
-        keyboardActions = KeyboardActions { validate(text) },
-        modifier = Modifier.semantics {
-            // Provide localized description of the error
-            if (isError) error("Email format is invalid.")
-        }
-    )
+    Column {
+        TextField(
+            value = text,
+            onValueChange = {
+                text = it
+                isError = false
+            },
+            singleLine = true,
+            label = { Text(if (isError) "Email*" else "Email") },
+            isError = isError,
+            keyboardActions = KeyboardActions { validate(text) },
+            modifier = Modifier.semantics {
+                // Provide localized description of the error
+                if (isError) error(errorMessage)
+            }
+        )
+        // Supporting text for error message.
+        Text(
+            text = errorMessage,
+            color = MaterialTheme.colorScheme.error,
+            style = MaterialTheme.typography.bodySmall,
+            modifier = Modifier.padding(start = 16.dp, top = 4.dp).alpha(if (isError) 1f else 0f)
+        )
+    }
 }
 
 @Sampled
 @Composable
-fun TextFieldWithHelperMessage() {
+fun TextFieldWithSupportingText() {
     var text by rememberSaveable { mutableStateOf("") }
 
     Column {
@@ -153,10 +164,10 @@
             label = { Text("Label") }
         )
         Text(
-            text = "Helper message",
+            text = "Supporting text",
             color = MaterialTheme.colorScheme.onSurface,
             style = MaterialTheme.typography.bodySmall,
-            modifier = Modifier.padding(start = 16.dp)
+            modifier = Modifier.padding(start = 16.dp, top = 4.dp)
         )
     }
 }
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
index 6b3e7e0..29c37c0 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/IconButton.kt
@@ -16,6 +16,7 @@
 package androidx.compose.material3
 
 import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -65,6 +66,8 @@
  * [Interaction]s for this icon button. You can create and pass in your own remembered
  * [MutableInteractionSource] to observe [Interaction]s that will customize the appearance
  * / behavior of this icon button in different states
+ * @param colors an [IconButtonColors] that will be used to resolve the colors used for this icon
+ * button in different states. See [IconButtonDefaults.iconButtonColors].
  * @param content the content (icon) to be drawn inside the icon button. This is typically an
  * [Icon].
  */
@@ -74,6 +77,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),
     content: @Composable () -> Unit
 ) {
     Box(
@@ -81,6 +85,10 @@
         modifier
             .minimumTouchTargetSize()
             .size(IconButtonTokens.StateLayerSize)
+            .background(
+                color = colors.containerColor(enabled).value,
+                IconButtonTokens.StateLayerShape.toShape()
+            )
             .clickable(
                 onClick = onClick,
                 enabled = enabled,
@@ -93,13 +101,7 @@
             ),
         contentAlignment = Alignment.Center
     ) {
-        val contentColor =
-            if (enabled) {
-                IconButtonTokens.UnselectedIconColor.toColor()
-            } else {
-                IconButtonTokens.DisabledIconColor.toColor()
-                    .copy(alpha = IconButtonTokens.DisabledIconOpacity)
-            }
+        val contentColor = colors.contentColor(enabled).value
         CompositionLocalProvider(LocalContentColor provides contentColor, content = content)
     }
 }
@@ -128,6 +130,8 @@
  * [Interaction]s for this icon button. You can create and pass in your own remembered
  * [MutableInteractionSource] to observe [Interaction]s that will customize the appearance
  * / behavior of this icon button in different states
+ * @param colors an [IconToggleButtonColors] that will be used to resolve the colors used for this
+ * icon button in different states. See [IconButtonDefaults.iconToggleButtonColors].
  * @param content the content (icon) to be drawn inside the icon button. This is typically an
  * [Icon].
  */
@@ -138,6 +142,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    colors: IconToggleButtonColors = IconButtonDefaults.iconToggleButtonColors(),
     content: @Composable () -> Unit
 ) {
     Box(
@@ -145,6 +150,10 @@
         modifier
             .minimumTouchTargetSize()
             .size(IconButtonTokens.StateLayerSize)
+            .background(
+                color = colors.containerColor(enabled, checked).value,
+                IconButtonTokens.StateLayerShape.toShape()
+            )
             .toggleable(
                 value = checked,
                 onValueChange = onCheckedChange,
@@ -158,12 +167,7 @@
             ),
         contentAlignment = Alignment.Center
     ) {
-        val contentColor = when {
-            !enabled -> IconButtonTokens.DisabledIconColor.toColor()
-                .copy(alpha = IconButtonTokens.DisabledIconOpacity)
-            !checked -> IconButtonTokens.UnselectedIconColor.toColor()
-            else -> IconButtonTokens.SelectedIconColor.toColor()
-        }
+        val contentColor = colors.contentColor(enabled, checked).value
         CompositionLocalProvider(LocalContentColor provides contentColor, content = content)
     }
 }
@@ -205,7 +209,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = FilledIconButtonTokens.ContainerShape,
+    shape: Shape = FilledIconButtonTokens.ContainerShape.toShape(),
     colors: IconButtonColors = IconButtonDefaults.filledIconButtonColors(),
     content: @Composable () -> Unit
 ) = Surface(
@@ -267,7 +271,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = FilledIconButtonTokens.ContainerShape,
+    shape: Shape = FilledIconButtonTokens.ContainerShape.toShape(),
     colors: IconButtonColors = IconButtonDefaults.filledTonalIconButtonColors(),
     content: @Composable () -> Unit
 ) = Surface(
@@ -326,7 +330,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = FilledIconButtonTokens.ContainerShape,
+    shape: Shape = FilledIconButtonTokens.ContainerShape.toShape(),
     colors: IconToggleButtonColors = IconButtonDefaults.filledIconToggleButtonColors(),
     content: @Composable () -> Unit
 ) = Surface(
@@ -391,7 +395,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = FilledIconButtonTokens.ContainerShape,
+    shape: Shape = FilledIconButtonTokens.ContainerShape.toShape(),
     colors: IconToggleButtonColors = IconButtonDefaults.filledTonalIconToggleButtonColors(),
     content: @Composable () -> Unit
 ) = Surface(
@@ -455,7 +459,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = OutlinedIconButtonTokens.ContainerShape,
+    shape: Shape = OutlinedIconButtonTokens.ContainerShape.toShape(),
     border: BorderStroke? = IconButtonDefaults.outlinedIconButtonBorder(enabled),
     colors: IconButtonColors = IconButtonDefaults.outlinedIconButtonColors(),
     content: @Composable () -> Unit
@@ -517,7 +521,7 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
-    shape: Shape = OutlinedIconButtonTokens.ContainerShape,
+    shape: Shape = OutlinedIconButtonTokens.ContainerShape.toShape(),
     border: BorderStroke? = IconButtonDefaults.outlinedIconToggleButtonBorder(enabled, checked),
     colors: IconToggleButtonColors = IconButtonDefaults.outlinedIconToggleButtonColors(),
     content: @Composable () -> Unit
@@ -607,6 +611,59 @@
 object IconButtonDefaults {
 
     /**
+     * Creates a [IconButtonColors] that represents the default colors used in a [IconButton].
+     *
+     * @param containerColor the container color of this icon button when enabled.
+     * @param contentColor the content color of this icon button when enabled.
+     * @param disabledContainerColor the container color of this icon button when not enabled.
+     * @param disabledContentColor the content color of this icon button when not enabled.
+     */
+    @Composable
+    fun iconButtonColors(
+        containerColor: Color = Color.Transparent,
+        contentColor: Color = IconButtonTokens.UnselectedIconColor.toColor(),
+        disabledContainerColor: Color = Color.Transparent,
+        disabledContentColor: Color = IconButtonTokens.DisabledIconColor.toColor()
+            .copy(alpha = IconButtonTokens.DisabledIconOpacity)
+    ): IconButtonColors =
+        DefaultIconButtonColors(
+            containerColor = containerColor,
+            contentColor = contentColor,
+            disabledContainerColor = disabledContainerColor,
+            disabledContentColor = disabledContentColor,
+        )
+
+    /**
+     * Creates a [IconToggleButtonColors] that represents the default colors used in a
+     * [IconToggleButton].
+     *
+     * @param containerColor the container color of this icon button when enabled.
+     * @param contentColor the content color of this icon button when enabled.
+     * @param disabledContainerColor the container color of this icon button when not enabled.
+     * @param disabledContentColor the content color of this icon button when not enabled.
+     * @param checkedContainerColor the container color of this icon button when checked.
+     * @param checkedContentColor the content color of this icon button when checked.
+     */
+    @Composable
+    fun iconToggleButtonColors(
+        containerColor: Color = Color.Transparent,
+        contentColor: Color = IconButtonTokens.UnselectedIconColor.toColor(),
+        disabledContainerColor: Color = Color.Transparent,
+        disabledContentColor: Color = IconButtonTokens.DisabledIconColor.toColor()
+            .copy(alpha = IconButtonTokens.DisabledIconOpacity),
+        checkedContainerColor: Color = Color.Transparent,
+        checkedContentColor: Color = IconButtonTokens.SelectedIconColor.toColor()
+    ): IconToggleButtonColors =
+        DefaultIconToggleButtonColors(
+            containerColor = containerColor,
+            contentColor = contentColor,
+            disabledContainerColor = disabledContainerColor,
+            disabledContentColor = disabledContentColor,
+            checkedContainerColor = checkedContainerColor,
+            checkedContentColor = checkedContentColor,
+        )
+
+    /**
      * Creates a [IconButtonColors] that represents the default colors used in a [FilledIconButton].
      *
      * @param containerColor the container color of this icon button when enabled.
@@ -632,7 +689,7 @@
 
     /**
      * Creates a [IconToggleButtonColors] that represents the default colors used in a
-     * toggleable [FilledIconButton].
+     * [FilledIconToggleButton].
      *
      * @param containerColor the container color of this icon button when enabled.
      * @param contentColor the content color of this icon button when enabled.
@@ -664,7 +721,8 @@
         )
 
     /**
-     * Creates a [IconButtonColors] that represents the default colors used in a [FilledIconButton].
+     * Creates a [IconButtonColors] that represents the default colors used in a
+     * [FilledTonalIconButton].
      *
      * @param containerColor the container color of this icon button when enabled.
      * @param contentColor the content color of this icon button when enabled.
@@ -689,7 +747,7 @@
 
     /**
      * Creates a [IconToggleButtonColors] that represents the default colors used in a
-     * [FilledIconButton].
+     * [FilledTonalIconToggleButton].
      *
      * @param containerColor the container color of this icon button when enabled.
      * @param contentColor the content color of this icon button when enabled.
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
index 9759be1..262b681 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
@@ -94,7 +94,7 @@
  *
  * Additionally, you may provide additional message at the bottom:
  *
- * @sample androidx.compose.material3.samples.TextFieldWithHelperMessage
+ * @sample androidx.compose.material3.samples.TextFieldWithSupportingText
  *
  * Password text field example:
  *
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledIconButtonTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledIconButtonTokens.kt
index 7cc814e..f5fb952 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledIconButtonTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledIconButtonTokens.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-// VERSION: v0_92
+// VERSION: v0_93
 // GENERATED CODE - DO NOT MODIFY BY HAND
 
 package androidx.compose.material3.tokens
@@ -22,7 +22,7 @@
 
 internal object FilledIconButtonTokens {
     val ContainerColor = ColorSchemeKeyTokens.Primary
-    val ContainerShape = ShapeTokens.CornerFull
+    val ContainerShape = ShapeKeyTokens.CornerFull
     val ContainerSize = 40.0.dp
     val DisabledContainerColor = ColorSchemeKeyTokens.OnSurface
     const val DisabledContainerOpacity = 0.12f
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledTonalIconButtonTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledTonalIconButtonTokens.kt
index c82f8c3..cbb9d40 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledTonalIconButtonTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/FilledTonalIconButtonTokens.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-// VERSION: v0_92
+// VERSION: v0_93
 // GENERATED CODE - DO NOT MODIFY BY HAND
 
 package androidx.compose.material3.tokens
@@ -22,7 +22,7 @@
 
 internal object FilledTonalIconButtonTokens {
     val ContainerColor = ColorSchemeKeyTokens.SecondaryContainer
-    val ContainerShape = ShapeTokens.CornerFull
+    val ContainerShape = ShapeKeyTokens.CornerFull
     val ContainerSize = 40.0.dp
     val DisabledContainerColor = ColorSchemeKeyTokens.OnSurface
     const val DisabledContainerOpacity = 0.12f
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/IconButtonTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/IconButtonTokens.kt
index afec766..6cc1bc1 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/IconButtonTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/IconButtonTokens.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * Copyright 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-// VERSION: v0_92
+// VERSION: v0_93
 // GENERATED CODE - DO NOT MODIFY BY HAND
 
 package androidx.compose.material3.tokens
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/OutlinedIconButtonTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/OutlinedIconButtonTokens.kt
index c30f605..cff5ada 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/OutlinedIconButtonTokens.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/OutlinedIconButtonTokens.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-// VERSION: v0_92
+// VERSION: v0_93
 // GENERATED CODE - DO NOT MODIFY BY HAND
 
 package androidx.compose.material3.tokens
@@ -21,7 +21,7 @@
 import androidx.compose.ui.unit.dp
 
 internal object OutlinedIconButtonTokens {
-    val ContainerShape = ShapeTokens.CornerFull
+    val ContainerShape = ShapeKeyTokens.CornerFull
     val ContainerSize = 40.0.dp
     val DisabledColor = ColorSchemeKeyTokens.OnSurface
     const val DisabledOpacity = 0.38f
diff --git a/fragment/fragment/api/1.5.0-beta01.txt b/fragment/fragment/api/1.5.0-beta01.txt
index 0a55a9e..32377c9 100644
--- a/fragment/fragment/api/1.5.0-beta01.txt
+++ b/fragment/fragment/api/1.5.0-beta01.txt
@@ -6,6 +6,7 @@
     ctor public DialogFragment(@LayoutRes int);
     method public void dismiss();
     method public void dismissAllowingStateLoss();
+    method @MainThread public void dismissNow();
     method public android.app.Dialog? getDialog();
     method public boolean getShowsDialog();
     method @StyleRes public int getTheme();
diff --git a/fragment/fragment/api/current.txt b/fragment/fragment/api/current.txt
index 0a55a9e..32377c9 100644
--- a/fragment/fragment/api/current.txt
+++ b/fragment/fragment/api/current.txt
@@ -6,6 +6,7 @@
     ctor public DialogFragment(@LayoutRes int);
     method public void dismiss();
     method public void dismissAllowingStateLoss();
+    method @MainThread public void dismissNow();
     method public android.app.Dialog? getDialog();
     method public boolean getShowsDialog();
     method @StyleRes public int getTheme();
diff --git a/fragment/fragment/api/public_plus_experimental_1.5.0-beta01.txt b/fragment/fragment/api/public_plus_experimental_1.5.0-beta01.txt
index 0a55a9e..32377c9 100644
--- a/fragment/fragment/api/public_plus_experimental_1.5.0-beta01.txt
+++ b/fragment/fragment/api/public_plus_experimental_1.5.0-beta01.txt
@@ -6,6 +6,7 @@
     ctor public DialogFragment(@LayoutRes int);
     method public void dismiss();
     method public void dismissAllowingStateLoss();
+    method @MainThread public void dismissNow();
     method public android.app.Dialog? getDialog();
     method public boolean getShowsDialog();
     method @StyleRes public int getTheme();
diff --git a/fragment/fragment/api/public_plus_experimental_current.txt b/fragment/fragment/api/public_plus_experimental_current.txt
index 0a55a9e..32377c9 100644
--- a/fragment/fragment/api/public_plus_experimental_current.txt
+++ b/fragment/fragment/api/public_plus_experimental_current.txt
@@ -6,6 +6,7 @@
     ctor public DialogFragment(@LayoutRes int);
     method public void dismiss();
     method public void dismissAllowingStateLoss();
+    method @MainThread public void dismissNow();
     method public android.app.Dialog? getDialog();
     method public boolean getShowsDialog();
     method @StyleRes public int getTheme();
diff --git a/fragment/fragment/api/restricted_1.5.0-beta01.txt b/fragment/fragment/api/restricted_1.5.0-beta01.txt
index 9e7cc35..7f0aa7c 100644
--- a/fragment/fragment/api/restricted_1.5.0-beta01.txt
+++ b/fragment/fragment/api/restricted_1.5.0-beta01.txt
@@ -6,6 +6,7 @@
     ctor public DialogFragment(@LayoutRes int);
     method public void dismiss();
     method public void dismissAllowingStateLoss();
+    method @MainThread public void dismissNow();
     method public android.app.Dialog? getDialog();
     method public boolean getShowsDialog();
     method @StyleRes public int getTheme();
diff --git a/fragment/fragment/api/restricted_current.txt b/fragment/fragment/api/restricted_current.txt
index 9e7cc35..7f0aa7c 100644
--- a/fragment/fragment/api/restricted_current.txt
+++ b/fragment/fragment/api/restricted_current.txt
@@ -6,6 +6,7 @@
     ctor public DialogFragment(@LayoutRes int);
     method public void dismiss();
     method public void dismissAllowingStateLoss();
+    method @MainThread public void dismissNow();
     method public android.app.Dialog? getDialog();
     method public boolean getShowsDialog();
     method @StyleRes public int getTheme();
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
index c582362..8ae92e1 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
@@ -66,6 +66,12 @@
     }
 }
 
+object FragmentDismissNow : Operation() {
+    override fun run(dialogFragment: DialogFragment) {
+        dialogFragment.dismissNow()
+    }
+}
+
 @LargeTest
 @RunWith(Parameterized::class)
 class DialogFragmentDismissTest(
@@ -87,6 +93,8 @@
                 // Run the operation off the main thread
                 add(arrayOf(operation, false))
             }
+            // dismissNow can only be run on the main thread
+            add(arrayOf(FragmentDismissNow, true))
         }
     }
 
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
index 92ec098..dfd3fbc 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
@@ -113,6 +113,27 @@
 
     @UiThreadTest
     @Test
+    fun testDialogFragmentDismissNow() {
+        val fragment = TestDialogFragment()
+        fragment.show(activityTestRule.activity.supportFragmentManager, null)
+        activityTestRule.runOnUiThread {
+            activityTestRule.activity.supportFragmentManager.executePendingTransactions()
+        }
+
+        val dialog = fragment.dialog
+        assertWithMessage("Dialog was not being shown")
+            .that(dialog?.isShowing)
+            .isTrue()
+
+        fragment.dismissNow()
+
+        assertWithMessage("Dialog should be removed")
+            .that(dialog?.isShowing)
+            .isFalse()
+    }
+
+    @UiThreadTest
+    @Test
     fun testDialogFragmentDismissAllowingStateLoss() {
         val viewModelStore = ViewModelStore()
         val fc = activityTestRule.startupFragmentController(viewModelStore)
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java
index 1477da4..7cf9874 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/DialogFragment.java
@@ -307,7 +307,16 @@
      * the fragment.
      */
     public void dismiss() {
-        dismissInternal(false, false);
+        dismissInternal(false, false, false);
+    }
+
+    /**
+     * Version of {@link #dismiss()} that uses {@link FragmentTransaction#commitNow()}.
+     * See linked documentation for further details.
+     */
+    @MainThread
+    public void dismissNow() {
+        dismissInternal(false, false, true);
     }
 
     /**
@@ -317,10 +326,10 @@
      * documentation for further details.
      */
     public void dismissAllowingStateLoss() {
-        dismissInternal(true, false);
+        dismissInternal(true, false, false);
     }
 
-    private void dismissInternal(boolean allowStateLoss, boolean fromOnDismiss) {
+    private void dismissInternal(boolean allowStateLoss, boolean fromOnDismiss, boolean immediate) {
         if (mDismissed) {
             return;
         }
@@ -346,14 +355,22 @@
         }
         mViewDestroyed = true;
         if (mBackStackId >= 0) {
-            getParentFragmentManager().popBackStack(mBackStackId,
-                    FragmentManager.POP_BACK_STACK_INCLUSIVE, allowStateLoss);
+            if (immediate) {
+                getParentFragmentManager().popBackStackImmediate(mBackStackId,
+                        FragmentManager.POP_BACK_STACK_INCLUSIVE);
+            } else {
+                getParentFragmentManager().popBackStack(mBackStackId,
+                        FragmentManager.POP_BACK_STACK_INCLUSIVE, allowStateLoss);
+            }
             mBackStackId = -1;
         } else {
             FragmentTransaction ft = getParentFragmentManager().beginTransaction();
             ft.setReorderingAllowed(true);
             ft.remove(this);
-            if (allowStateLoss) {
+            // allowStateLoss and immediate should not both be true
+            if (immediate) {
+                ft.commitNow();
+            } else if (allowStateLoss) {
                 ft.commitAllowingStateLoss();
             } else {
                 ft.commit();
@@ -633,7 +650,7 @@
             if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
                 Log.d(TAG, "onDismiss called for DialogFragment " + this);
             }
-            dismissInternal(true, true);
+            dismissInternal(true, true, false);
         }
     }
 
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 5c424fe..f598eae 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -105,6 +105,8 @@
  */
 public abstract class FragmentManager implements FragmentResultOwner {
     static final String SAVED_STATE_TAG = "android:support:fragments";
+    static final String FRAGMENT_MANAGER_STATE_TAG = "state";
+    static final String RESULT_NAME_PREFIX = "result_";
     private static boolean DEBUG = false;
 
     /** @hide */
@@ -2347,10 +2349,13 @@
             throwException(new IllegalStateException("You cannot use saveAllState when your "
                     + "FragmentHostCallback implements SavedStateRegistryOwner."));
         }
-        return saveAllStateInternal();
+        Bundle savedState = saveAllStateInternal();
+        return savedState.isEmpty() ? null : savedState;
     }
 
-    Parcelable saveAllStateInternal() {
+    @NonNull
+    Bundle saveAllStateInternal() {
+        Bundle bundle = new Bundle();
         // Make sure all pending operations have now been executed to get
         // our state update-to-date.
         forcePostponedTransactions();
@@ -2365,46 +2370,50 @@
 
         // And grab all FragmentState objects
         ArrayList<FragmentState> savedState = mFragmentStore.getAllSavedState();
-
         if (savedState.isEmpty()) {
-            if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "saveAllState: no fragments!");
-            return null;
-        }
+            if (isLoggingEnabled(Log.VERBOSE)) {
+                Log.v(TAG, "saveAllState: no fragments!");
+            }
+        } else {
+            // Build list of currently added fragments.
+            ArrayList<String> added = mFragmentStore.saveAddedFragments();
 
-        // Build list of currently added fragments.
-        ArrayList<String> added = mFragmentStore.saveAddedFragments();
-
-        // Now save back stack.
-        BackStackRecordState[] backStack = null;
-        if (mBackStack != null) {
-            int size = mBackStack.size();
-            if (size > 0) {
-                backStack = new BackStackRecordState[size];
-                for (int i = 0; i < size; i++) {
-                    backStack[i] = new BackStackRecordState(mBackStack.get(i));
-                    if (isLoggingEnabled(Log.VERBOSE)) {
-                        Log.v(TAG, "saveAllState: adding back stack #" + i
-                                + ": " + mBackStack.get(i));
+            // Now save back stack.
+            BackStackRecordState[] backStack = null;
+            if (mBackStack != null) {
+                int size = mBackStack.size();
+                if (size > 0) {
+                    backStack = new BackStackRecordState[size];
+                    for (int i = 0; i < size; i++) {
+                        backStack[i] = new BackStackRecordState(mBackStack.get(i));
+                        if (isLoggingEnabled(Log.VERBOSE)) {
+                            Log.v(TAG, "saveAllState: adding back stack #" + i
+                                    + ": " + mBackStack.get(i));
+                        }
                     }
                 }
             }
+
+            FragmentManagerState fms = new FragmentManagerState();
+            fms.mSavedState = savedState;
+            fms.mActive = active;
+            fms.mAdded = added;
+            fms.mBackStack = backStack;
+            fms.mBackStackIndex = mBackStackIndex.get();
+            if (mPrimaryNav != null) {
+                fms.mPrimaryNavActiveWho = mPrimaryNav.mWho;
+            }
+            fms.mBackStackStateKeys.addAll(mBackStackStates.keySet());
+            fms.mBackStackStates.addAll(mBackStackStates.values());
+            fms.mLaunchedFragments = new ArrayList<>(mLaunchedFragments);
+            bundle.putParcelable(FRAGMENT_MANAGER_STATE_TAG, fms);
+
+            for (String resultName : mResults.keySet()) {
+                bundle.putBundle(RESULT_NAME_PREFIX + resultName, mResults.get(resultName));
+            }
         }
 
-        FragmentManagerState fms = new FragmentManagerState();
-        fms.mSavedState = savedState;
-        fms.mActive = active;
-        fms.mAdded = added;
-        fms.mBackStack = backStack;
-        fms.mBackStackIndex = mBackStackIndex.get();
-        if (mPrimaryNav != null) {
-            fms.mPrimaryNavActiveWho = mPrimaryNav.mWho;
-        }
-        fms.mBackStackStateKeys.addAll(mBackStackStates.keySet());
-        fms.mBackStackStates.addAll(mBackStackStates.values());
-        fms.mResultKeys.addAll(mResults.keySet());
-        fms.mResults.addAll(mResults.values());
-        fms.mLaunchedFragments = new ArrayList<>(mLaunchedFragments);
-        return fms;
+        return bundle;
     }
 
     @SuppressWarnings("deprecation")
@@ -2425,11 +2434,25 @@
         restoreSaveStateInternal(state);
     }
 
+    @SuppressWarnings("deprecation")
     void restoreSaveStateInternal(@Nullable Parcelable state) {
         // If there is no saved state at all, then there's nothing else to do
         if (state == null) return;
-        FragmentManagerState fms = (FragmentManagerState) state;
-        if (fms.mSavedState == null) return;
+        Bundle bundle = (Bundle) state;
+
+        for (String bundleKey : bundle.keySet()) {
+            if (bundleKey.startsWith(RESULT_NAME_PREFIX)) {
+                Bundle savedResult = bundle.getBundle(bundleKey);
+                if (savedResult != null) {
+                    savedResult.setClassLoader(mHost.getContext().getClassLoader());
+                    String resultKey = bundleKey.substring(RESULT_NAME_PREFIX.length());
+                    mResults.put(resultKey, savedResult);
+                }
+            }
+        }
+
+        FragmentManagerState fms = bundle.getParcelable(FRAGMENT_MANAGER_STATE_TAG);
+        if (fms == null || fms.mSavedState == null) return;
 
         // Restore the saved state of all fragments
         mFragmentStore.restoreSaveState(fms.mSavedState);
@@ -2526,14 +2549,6 @@
             }
         }
 
-        ArrayList<String> savedResultKeys = fms.mResultKeys;
-        if (savedResultKeys != null) {
-            for (int i = 0; i < savedResultKeys.size(); i++) {
-                Bundle savedResult = fms.mResults.get(i);
-                savedResult.setClassLoader(mHost.getContext().getClassLoader());
-                mResults.put(savedResultKeys.get(i), savedResult);
-            }
-        }
         mLaunchedFragments = new ArrayDeque<>(fms.mLaunchedFragments);
     }
 
@@ -2614,20 +2629,14 @@
             SavedStateRegistry registry =
                     ((SavedStateRegistryOwner) mHost).getSavedStateRegistry();
             registry.registerSavedStateProvider(SAVED_STATE_TAG, () -> {
-                        Bundle outState = new Bundle();
-                        Parcelable p = saveAllStateInternal();
-                        if (p != null) {
-                            outState.putParcelable(SAVED_STATE_TAG, p);
-                        }
-                        return outState;
+                        return saveAllStateInternal();
                     }
             );
 
             Bundle savedInstanceState = registry
                     .consumeRestoredStateForKey(SAVED_STATE_TAG);
             if (savedInstanceState != null) {
-                Parcelable p = savedInstanceState.getParcelable(SAVED_STATE_TAG);
-                restoreSaveStateInternal(p);
+                restoreSaveStateInternal(savedInstanceState);
             }
         }
 
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java
index a90909a..117e31c 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java
@@ -17,7 +17,6 @@
 package androidx.fragment.app;
 
 import android.annotation.SuppressLint;
-import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -33,8 +32,6 @@
     String mPrimaryNavActiveWho = null;
     ArrayList<String> mBackStackStateKeys = new ArrayList<>();
     ArrayList<BackStackState> mBackStackStates = new ArrayList<>();
-    ArrayList<String> mResultKeys = new ArrayList<>();
-    ArrayList<Bundle> mResults = new ArrayList<>();
     ArrayList<FragmentManager.LaunchedFragmentInfo> mLaunchedFragments;
 
     public FragmentManagerState() {
@@ -49,8 +46,6 @@
         mPrimaryNavActiveWho = in.readString();
         mBackStackStateKeys = in.createStringArrayList();
         mBackStackStates = in.createTypedArrayList(BackStackState.CREATOR);
-        mResultKeys = in.createStringArrayList();
-        mResults = in.createTypedArrayList(Bundle.CREATOR);
         mLaunchedFragments = in.createTypedArrayList(FragmentManager.LaunchedFragmentInfo.CREATOR);
     }
 
@@ -69,8 +64,6 @@
         dest.writeString(mPrimaryNavActiveWho);
         dest.writeStringList(mBackStackStateKeys);
         dest.writeTypedList(mBackStackStates);
-        dest.writeStringList(mResultKeys);
-        dest.writeTypedList(mResults);
         dest.writeTypedList(mLaunchedFragments);
     }
 
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index f426772..431a244 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -13,6 +13,7 @@
          <trust group="^com[.]android($|([.].*))" regex="true"/> <!-- b/215430394 -->
          <trust group="com.google.testing.platform"/> <!-- b/215430394 -->
          <trust group="com.google.android.gms"/> <!-- b/215442095 -->
+         <trust file=".*kotlin-native-prebuilt-macos-.*" regex="true"/> <!-- b/228184608 -->
       </trusted-artifacts>
       <trusted-keys>
          <trusted-key id="00089ee8c3afa95a854d0f1df800dd0933ecf7f7" group="com.google.guava" name="guava"/>
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/datatype/RecordsTypeNameMap.kt b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/datatype/RecordsTypeNameMap.kt
index 0cb371e..29f46ee 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/datatype/RecordsTypeNameMap.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/datatype/RecordsTypeNameMap.kt
@@ -33,7 +33,7 @@
 import androidx.health.data.client.records.Distance
 import androidx.health.data.client.records.ElevationGained
 import androidx.health.data.client.records.FloorsClimbed
-import androidx.health.data.client.records.HeartRate
+import androidx.health.data.client.records.HeartRateSeries
 import androidx.health.data.client.records.HeartRateVariabilityDifferentialIndex
 import androidx.health.data.client.records.HeartRateVariabilityRmssd
 import androidx.health.data.client.records.HeartRateVariabilityS
@@ -52,6 +52,7 @@
 import androidx.health.data.client.records.OvulationTest
 import androidx.health.data.client.records.OxygenSaturation
 import androidx.health.data.client.records.Power
+import androidx.health.data.client.records.Record
 import androidx.health.data.client.records.Repetitions
 import androidx.health.data.client.records.RespiratoryRate
 import androidx.health.data.client.records.RestingHeartRate
@@ -67,6 +68,7 @@
 import androidx.health.data.client.records.WaistCircumference
 import androidx.health.data.client.records.Weight
 import androidx.health.data.client.records.WheelchairPushes
+import kotlin.reflect.KClass
 
 private val ALL_RECORDS_TYPES =
     setOf(
@@ -88,7 +90,7 @@
         Distance::class,
         ElevationGained::class,
         FloorsClimbed::class,
-        HeartRate::class,
+        HeartRateSeries::class,
         HeartRateVariabilityDifferentialIndex::class,
         HeartRateVariabilityRmssd::class,
         HeartRateVariabilityS::class,
@@ -124,4 +126,5 @@
         Weight::class,
     )
 
-val RECORDS_TYPE_NAME_MAP = ALL_RECORDS_TYPES.associateBy { it.simpleName!! }
+val RECORDS_TYPE_NAME_MAP: Map<String, KClass<out Record>> =
+    ALL_RECORDS_TYPES.associateBy { it.simpleName!! }
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordConverters.kt b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordConverters.kt
index 7eed099..bfefa93 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordConverters.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordConverters.kt
@@ -34,6 +34,7 @@
 import androidx.health.data.client.records.ElevationGained
 import androidx.health.data.client.records.FloorsClimbed
 import androidx.health.data.client.records.HeartRate
+import androidx.health.data.client.records.HeartRateSeries
 import androidx.health.data.client.records.HeartRateVariabilityDifferentialIndex
 import androidx.health.data.client.records.HeartRateVariabilityRmssd
 import androidx.health.data.client.records.HeartRateVariabilityS
@@ -69,11 +70,11 @@
 import androidx.health.data.client.records.Weight
 import androidx.health.data.client.records.WheelchairPushes
 import androidx.health.platform.client.proto.DataProto
-import java.lang.RuntimeException
+import java.time.Instant
 
 /** Converts public API object into internal proto for ipc. */
-fun toRecord(proto: DataProto.DataPoint): Record {
-    return with(proto) {
+fun toRecord(proto: DataProto.DataPoint): Record =
+    with(proto) {
         when (dataType.name) {
             "BasalBodyTemperature" ->
                 BasalBodyTemperature(
@@ -163,12 +164,20 @@
                     zoneOffset = zoneOffset,
                     metadata = metadata
                 )
-            "HeartRate" ->
-                HeartRate(
-                    beatsPerMinute = getLong("bpm"),
-                    time = time,
-                    zoneOffset = zoneOffset,
-                    metadata = metadata
+            "HeartRateSeries" ->
+                HeartRateSeries(
+                    startTime = startTime,
+                    startZoneOffset = startZoneOffset,
+                    endTime = endTime,
+                    endZoneOffset = endZoneOffset,
+                    samples =
+                        seriesValuesList.map { value ->
+                            HeartRate(
+                                time = Instant.ofEpochMilli(value.instantTimeMillis),
+                                beatsPerMinute = value.getLong("bpm"),
+                            )
+                        },
+                    metadata = metadata,
                 )
             "Height" ->
                 Height(
@@ -533,4 +542,3 @@
             else -> throw RuntimeException("Unknown data type ${dataType.name}")
         }
     }
-}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordUtils.kt b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordUtils.kt
index 4ad70d8..bbd4a24 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordUtils.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/ProtoToRecordUtils.kt
@@ -19,52 +19,60 @@
 import androidx.health.data.client.metadata.Device
 import androidx.health.data.client.metadata.Metadata
 import androidx.health.platform.client.proto.DataProto
+import androidx.health.platform.client.proto.DataProto.DataPointOrBuilder
+import androidx.health.platform.client.proto.DataProto.SeriesValueOrBuilder
 import java.time.Instant
 import java.time.ZoneOffset
 
 /** Internal helper functions to convert proto to records. */
-@get:SuppressWarnings("GoodTime", "NewApi") // Safe to use for deserialization
+@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
 internal val DataProto.DataPoint.startTime: Instant
     get() = Instant.ofEpochMilli(startTimeMillis)
 
-@get:SuppressWarnings("GoodTime", "NewApi") // Safe to use for deserialization
+@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
 internal val DataProto.DataPoint.endTime: Instant
     get() = Instant.ofEpochMilli(endTimeMillis)
 
-@get:SuppressWarnings("GoodTime", "NewApi") // Safe to use for deserialization
+@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
 internal val DataProto.DataPoint.time: Instant
     get() = Instant.ofEpochMilli(instantTimeMillis)
 
-@get:SuppressWarnings("GoodTime", "NewApi") // Safe to use for deserialization
+@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
 internal val DataProto.DataPoint.startZoneOffset: ZoneOffset?
     get() =
         if (hasStartZoneOffsetSeconds()) ZoneOffset.ofTotalSeconds(startZoneOffsetSeconds) else null
 
-@get:SuppressWarnings("GoodTime", "NewApi") // Safe to use for deserialization
+@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
 internal val DataProto.DataPoint.endZoneOffset: ZoneOffset?
     get() = if (hasEndZoneOffsetSeconds()) ZoneOffset.ofTotalSeconds(endZoneOffsetSeconds) else null
 
-@get:SuppressWarnings("GoodTime", "NewApi") // Safe to use for deserialization
+@get:SuppressWarnings("GoodTime") // HealthDataClientImplSafe to use for deserialization
 internal val DataProto.DataPoint.zoneOffset: ZoneOffset?
     get() = if (hasZoneOffsetSeconds()) ZoneOffset.ofTotalSeconds(zoneOffsetSeconds) else null
 
-internal fun DataProto.DataPoint.getLong(key: String, defaultVal: Long = 0): Long {
-    return valuesMap[key]?.longVal ?: defaultVal
-}
+internal fun DataPointOrBuilder.getLong(key: String, defaultVal: Long = 0): Long =
+    valuesMap[key]?.longVal ?: defaultVal
 
-internal fun DataProto.DataPoint.getDouble(key: String, defaultVal: Double = 0.0): Double {
-    return valuesMap[key]?.doubleVal ?: defaultVal
-}
+internal fun DataPointOrBuilder.getDouble(key: String, defaultVal: Double = 0.0): Double =
+    valuesMap[key]?.doubleVal ?: defaultVal
 
-internal fun DataProto.DataPoint.getString(key: String): String? {
-    return valuesMap[key]?.stringVal
-}
+internal fun DataPointOrBuilder.getString(key: String): String? = valuesMap[key]?.stringVal
 
-internal fun DataProto.DataPoint.getEnum(key: String): String? {
+internal fun DataPointOrBuilder.getEnum(key: String): String? {
     return valuesMap[key]?.enumVal
 }
 
-@get:SuppressWarnings("GoodTime", "NewApi") // Safe to use for deserialization
+internal fun SeriesValueOrBuilder.getLong(key: String, defaultVal: Long = 0): Long =
+    valuesMap[key]?.longVal ?: defaultVal
+
+internal fun SeriesValueOrBuilder.getDouble(key: String, defaultVal: Double = 0.0): Double =
+    valuesMap[key]?.doubleVal ?: defaultVal
+
+internal fun SeriesValueOrBuilder.getString(key: String): String? = valuesMap[key]?.stringVal
+
+internal fun SeriesValueOrBuilder.getEnum(key: String): String? = valuesMap[key]?.enumVal
+
+@get:SuppressWarnings("GoodTime") // Safe to use for deserialization
 internal val DataProto.DataPoint.metadata: Metadata
     get() =
         Metadata(
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoConverters.kt b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoConverters.kt
index 2b78f78..0d59e11 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoConverters.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoConverters.kt
@@ -33,7 +33,7 @@
 import androidx.health.data.client.records.Distance
 import androidx.health.data.client.records.ElevationGained
 import androidx.health.data.client.records.FloorsClimbed
-import androidx.health.data.client.records.HeartRate
+import androidx.health.data.client.records.HeartRateSeries
 import androidx.health.data.client.records.HeartRateVariabilityDifferentialIndex
 import androidx.health.data.client.records.HeartRateVariabilityRmssd
 import androidx.health.data.client.records.HeartRateVariabilityS
@@ -56,6 +56,7 @@
 import androidx.health.data.client.records.Repetitions
 import androidx.health.data.client.records.RespiratoryRate
 import androidx.health.data.client.records.RestingHeartRate
+import androidx.health.data.client.records.SeriesRecord
 import androidx.health.data.client.records.SexualActivity
 import androidx.health.data.client.records.SleepSession
 import androidx.health.data.client.records.SleepStage
@@ -69,12 +70,10 @@
 import androidx.health.data.client.records.Weight
 import androidx.health.data.client.records.WheelchairPushes
 import androidx.health.platform.client.proto.DataProto
-import java.lang.RuntimeException
 
 /** Converts public API object into internal proto for ipc. */
-@SuppressWarnings("NewApi") // Safe to use with java8 desugar
-fun Record.toProto(): DataProto.DataPoint {
-    return when (this) {
+fun Record.toProto(): DataProto.DataPoint =
+    when (this) {
         is BasalBodyTemperature ->
             instantaneousProto()
                 .setDataType(protoDataType("BasalBodyTemperature"))
@@ -153,11 +152,13 @@
                 .setDataType(protoDataType("CyclingPedalingCadence"))
                 .apply { putValues("rpm", doubleVal(revolutionsPerMinute)) }
                 .build()
-        is HeartRate ->
-            instantaneousProto()
-                .setDataType(protoDataType("HeartRate"))
-                .apply { putValues("bpm", longVal(beatsPerMinute)) }
-                .build()
+        is HeartRateSeries ->
+            toProto(dataTypeName = "HeartRateSeries") { sample ->
+                DataProto.SeriesValue.newBuilder()
+                    .putValues("bpm", longVal(sample.beatsPerMinute))
+                    .setInstantTimeMillis(sample.time.toEpochMilli())
+                    .build()
+            }
         is Height ->
             instantaneousProto()
                 .setDataType(protoDataType("Height"))
@@ -508,4 +509,16 @@
                 .build()
         else -> throw RuntimeException("Unsupported yet!")
     }
-}
+
+private fun <T : Any> SeriesRecord<T>.toProto(
+    dataTypeName: String,
+    getSeriesValue: (sample: T) -> DataProto.SeriesValue,
+): DataProto.DataPoint =
+    intervalProto()
+        .setDataType(protoDataType(dataTypeName = dataTypeName))
+        .apply {
+            for (sample in samples) {
+                addSeriesValues(getSeriesValue(sample))
+            }
+        }
+        .build()
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoUtils.kt b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoUtils.kt
index a882c33..bb2802b 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoUtils.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/impl/converters/records/RecordToProtoUtils.kt
@@ -25,7 +25,7 @@
 internal fun protoDataType(dataTypeName: String): DataProto.DataType =
     DataProto.DataType.newBuilder().setName(dataTypeName).build()
 
-@SuppressWarnings("GoodTime", "NewApi") // Suppress GoodTime for serialize/de-serialize.
+@SuppressWarnings("GoodTime") // Suppress GoodTime for serialize/de-serialize.
 internal fun InstantaneousRecord.instantaneousProto(): DataProto.DataPoint.Builder {
     val builder =
         DataProto.DataPoint.newBuilder()
@@ -35,7 +35,7 @@
     return builder
 }
 
-@SuppressWarnings("GoodTime", "NewApi") // Suppress GoodTime for serialize/de-serialize.
+@SuppressWarnings("GoodTime") // Suppress GoodTime for serialize/de-serialize.
 internal fun IntervalRecord.intervalProto(): DataProto.DataPoint.Builder {
     val builder =
         DataProto.DataPoint.newBuilder()
@@ -47,7 +47,7 @@
     return builder
 }
 
-@SuppressWarnings("GoodTime", "NewApi") // Suppress GoodTime for serialize/de-serialize.
+@SuppressWarnings("GoodTime") // Suppress GoodTime for serialize/de-serialize.
 private fun DataProto.DataPoint.Builder.setMetadata(metadata: Metadata) = apply {
     metadata.uid?.let { setUid(it) }
     if (metadata.dataOrigin.packageName.isNotEmpty()) {
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/BloodPressure.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/BloodPressure.kt
index bd49bfd..820fd3a 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/BloodPressure.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/BloodPressure.kt
@@ -80,32 +80,32 @@
 
     companion object {
         /** Metric identifier to retrieve average systolic from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         val BLOOD_PRESSURE_SYSTOLIC_AVG: DoubleAggregateMetric =
             DoubleAggregateMetric("BloodPressure", "avg", "systolic")
 
         /** Metric identifier to retrieve minimum systolic from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         val BLOOD_PRESSURE_SYSTOLIC_MIN: DoubleAggregateMetric =
             DoubleAggregateMetric("BloodPressure", "min", "systolic")
 
         /** Metric identifier to retrieve maximum systolic from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         val BLOOD_PRESSURE_SYSTOLIC_MAX: DoubleAggregateMetric =
             DoubleAggregateMetric("BloodPressure", "max", "systolic")
 
         /** Metric identifier to retrieve average diastolic from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         val BLOOD_PRESSURE_DIASTOLIC_AVG: DoubleAggregateMetric =
             DoubleAggregateMetric("BloodPressure", "avg", "diastolic")
 
         /** Metric identifier to retrieve minimum diastolic from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         val BLOOD_PRESSURE_DIASTOLIC_MIN: DoubleAggregateMetric =
             DoubleAggregateMetric("BloodPressure", "min", "diastolic")
 
         /** Metric identifier to retrieve maximum diastolic from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         val BLOOD_PRESSURE_DIASTOLIC_MAX: DoubleAggregateMetric =
             DoubleAggregateMetric("BloodPressure", "max", "diastolic")
     }
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/Distance.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/Distance.kt
index 8cbb3ea..2fe3c53 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/Distance.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/Distance.kt
@@ -65,7 +65,7 @@
 
     internal companion object {
         /** Metric identifier to retrieve total distance from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         internal val DISTANCE_TOTAL: DoubleAggregateMetric =
             DoubleAggregateMetric("Distance", "total", "distance")
     }
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/ElevationGained.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/ElevationGained.kt
index f5886d14..f0731db8 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/ElevationGained.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/ElevationGained.kt
@@ -56,7 +56,7 @@
 
     internal companion object {
         /** Metric identifier to retrieve total elevation gained from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         internal val ELEVATION_TOTAL: DoubleAggregateMetric =
             DoubleAggregateMetric("ElevationGained", "total", "elevation")
     }
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/FloorsClimbed.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/FloorsClimbed.kt
index 585f62f..b6c5785d 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/FloorsClimbed.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/FloorsClimbed.kt
@@ -58,7 +58,7 @@
 
     companion object {
         /** Metric identifier to retrieve total floors climbed from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         val DISTANCE_TOTAL: DoubleAggregateMetric =
             DoubleAggregateMetric("FloorsClimbed", "total", "floors")
     }
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/HeartRate.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/HeartRate.kt
deleted file mode 100644
index 562d5e5..0000000
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/HeartRate.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.health.data.client.records
-
-import androidx.annotation.RestrictTo
-import androidx.health.data.client.aggregate.LongAggregateMetric
-import androidx.health.data.client.metadata.Metadata
-import java.time.Instant
-import java.time.ZoneOffset
-
-/** Captures the user's heart rate. Each record represents a single instantaneous measurement. */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public class HeartRate(
-    /** Heart beats per minute. Required field. Validation range: 1-300. */
-    public val beatsPerMinute: Long,
-    override val time: Instant,
-    override val zoneOffset: ZoneOffset?,
-    override val metadata: Metadata = Metadata.EMPTY,
-) : InstantaneousRecord {
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is HeartRate) return false
-
-        if (beatsPerMinute != other.beatsPerMinute) return false
-        if (time != other.time) return false
-        if (zoneOffset != other.zoneOffset) return false
-        if (metadata != other.metadata) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + beatsPerMinute.hashCode()
-        result = 31 * result + time.hashCode()
-        result = 31 * result + (zoneOffset?.hashCode() ?: 0)
-        result = 31 * result + metadata.hashCode()
-        return result
-    }
-
-    companion object {
-        /** Metric identifier to retrieve average heart rate from [AggregateDataRow]. */
-        @JvmStatic
-        val HEART_RATE_BPM_AVG: LongAggregateMetric = LongAggregateMetric("HeartRate", "avg", "bpm")
-
-        /** Metric identifier to retrieve minimum heart rate from [AggregateDataRow]. */
-        @JvmStatic
-        val HEART_RATE_BPM_MIN: LongAggregateMetric = LongAggregateMetric("HeartRate", "min", "bpm")
-
-        /** Metric identifier to retrieve maximum heart rate from [AggregateDataRow]. */
-        @JvmStatic
-        val HEART_RATE_BPM_MAX: LongAggregateMetric = LongAggregateMetric("HeartRate", "max", "bpm")
-    }
-}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/HeartRateSeries.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/HeartRateSeries.kt
new file mode 100644
index 0000000..e810ff5b
--- /dev/null
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/HeartRateSeries.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.health.data.client.records
+
+import androidx.annotation.RestrictTo
+import androidx.health.data.client.aggregate.LongAggregateMetric
+import androidx.health.data.client.metadata.Metadata
+import java.time.Instant
+import java.time.ZoneOffset
+
+/** Captures the user's heart rate. Each record represents a series of measurements. */
+@RestrictTo(RestrictTo.Scope.LIBRARY) // Will be made public after API reviews
+public class HeartRateSeries(
+    override val startTime: Instant,
+    override val startZoneOffset: ZoneOffset?,
+    override val endTime: Instant,
+    override val endZoneOffset: ZoneOffset?,
+    override val samples: List<HeartRate>,
+    override val metadata: Metadata = Metadata.EMPTY,
+) : SeriesRecord<HeartRate> {
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is HeartRateSeries) return false
+
+        if (startTime != other.startTime) return false
+        if (startZoneOffset != other.startZoneOffset) return false
+        if (endTime != other.endTime) return false
+        if (endZoneOffset != other.endZoneOffset) return false
+        if (samples != other.samples) return false
+        if (metadata != other.metadata) return false
+
+        return true
+    }
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
+    override fun hashCode(): Int {
+        var result = startTime.hashCode()
+        result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
+        result = 31 * result + endTime.hashCode()
+        result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
+        result = 31 * result + samples.hashCode()
+        result = 31 * result + metadata.hashCode()
+        return result
+    }
+
+    internal companion object {
+        /** Metric identifier to retrieve average heart rate from [AggregateDataRow]. */
+        @JvmField
+        internal val BPM_AVG: LongAggregateMetric = LongAggregateMetric("HeartRate", "avg", "bpm")
+
+        /** Metric identifier to retrieve minimum heart rate from [AggregateDataRow]. */
+        @JvmField
+        internal val BPM_MIN: LongAggregateMetric = LongAggregateMetric("HeartRate", "min", "bpm")
+
+        /** Metric identifier to retrieve maximum heart rate from [AggregateDataRow]. */
+        @JvmField
+        internal val BPM_MAX: LongAggregateMetric = LongAggregateMetric("HeartRate", "max", "bpm")
+    }
+}
+
+/**
+ * Represents a single measurement of the heart rate.
+ *
+ * @param beatsPerMinute Heart beats per minute. Validation range: 1-300.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY) // Will be made public after API reviews
+public class HeartRate(
+    val time: Instant,
+    val beatsPerMinute: Long,
+) {
+
+    /*
+     * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+     */
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is HeartRate) return false
+
+        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-data-client/src/main/java/androidx/health/data/client/records/Height.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/Height.kt
index 8d9b426..36f3f4c 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/Height.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/Height.kt
@@ -51,17 +51,17 @@
 
     internal companion object {
         /** Metric identifier to retrieve average height from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         internal val HEIGHT_AVG: DoubleAggregateMetric =
             DoubleAggregateMetric("Height", "avg", "height")
 
         /** Metric identifier to retrieve minimum height from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         internal val HEIGHT_MIN: DoubleAggregateMetric =
             DoubleAggregateMetric("Height", "min", "height")
 
         /** Metric identifier to retrieve maximum height from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         internal val HEIGHT_MAX: DoubleAggregateMetric =
             DoubleAggregateMetric("Height", "max", "height")
     }
diff --git a/health/health-data-client/src/main/java/androidx/health/platform/client/error/HealthDataException.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/SeriesRecord.kt
similarity index 67%
rename from health/health-data-client/src/main/java/androidx/health/platform/client/error/HealthDataException.kt
rename to health/health-data-client/src/main/java/androidx/health/data/client/records/SeriesRecord.kt
index e2d1362..e3c3a81 100644
--- a/health/health-data-client/src/main/java/androidx/health/platform/client/error/HealthDataException.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/SeriesRecord.kt
@@ -13,11 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.health.platform.client.error
+package androidx.health.data.client.records
 
-/**
- * Exception thrown by Health Platform. Contains one of [ErrorCode]s and message with details on the
- * error.
- */
-open class HealthDataException(val errorCode: Int, errorMessage: String? = "") :
-    Exception("$errorCode: $errorMessage")
+/** A record that contains a series of measurements. */
+// Consider exposing this one and other similar interfaces!
+internal interface SeriesRecord<out T : Any> : IntervalRecord {
+
+    val samples: List<T>
+}
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/Speed.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/Speed.kt
index f9e17dc..e0b0077 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/Speed.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/Speed.kt
@@ -57,15 +57,15 @@
 
     companion object {
         /** Metric identifier to retrieve average speed from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         val SPEED_AVG: DoubleAggregateMetric = DoubleAggregateMetric("Speed", "avg", "speed")
 
         /** Metric identifier to retrieve minimum speed from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         val SPEED_MIN: DoubleAggregateMetric = DoubleAggregateMetric("Speed", "min", "speed")
 
         /** Metric identifier to retrieve maximum speed from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         val SPEED_MAX: DoubleAggregateMetric = DoubleAggregateMetric("Speed", "max", "speed")
     }
 }
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/Steps.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/Steps.kt
index 18376eb..1b0c7c5 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/Steps.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/Steps.kt
@@ -64,7 +64,7 @@
 
     internal companion object {
         /** Metric identifier to retrieve total steps count from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         internal val STEPS_COUNT_TOTAL: LongAggregateMetric =
             LongAggregateMetric("Steps", "total", "count")
     }
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/TotalCaloriesBurned.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/TotalCaloriesBurned.kt
index dce8edd..87ef22f 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/TotalCaloriesBurned.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/TotalCaloriesBurned.kt
@@ -61,7 +61,7 @@
 
     companion object {
         /** Metric identifier to retrieve total energy from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         val ENERGY_BURNED_TOTAL: DoubleAggregateMetric =
             DoubleAggregateMetric("TotalEnergyBurned", "total", "energy")
     }
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/records/Weight.kt b/health/health-data-client/src/main/java/androidx/health/data/client/records/Weight.kt
index 244fb36..a07b814 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/records/Weight.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/records/Weight.kt
@@ -51,17 +51,17 @@
 
     internal companion object {
         /** Metric identifier to retrieve average weight from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         internal val WEIGHT_AVG: DoubleAggregateMetric =
             DoubleAggregateMetric("Weight", "avg", "weight")
 
         /** Metric identifier to retrieve minimum weight from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         internal val WEIGHT_MIN: DoubleAggregateMetric =
             DoubleAggregateMetric("Weight", "min", "weight")
 
         /** Metric identifier to retrieve maximum weight from [AggregateDataRow]. */
-        @JvmStatic
+        @JvmField
         internal val WEIGHT_MAX: DoubleAggregateMetric =
             DoubleAggregateMetric("Weight", "max", "weight")
     }
diff --git a/health/health-data-client/src/main/java/androidx/health/data/client/time/TimeRangeFilter.kt b/health/health-data-client/src/main/java/androidx/health/data/client/time/TimeRangeFilter.kt
index e9e1d48..8bd1300 100644
--- a/health/health-data-client/src/main/java/androidx/health/data/client/time/TimeRangeFilter.kt
+++ b/health/health-data-client/src/main/java/androidx/health/data/client/time/TimeRangeFilter.kt
@@ -146,4 +146,25 @@
 
     internal fun isOpenEnded(): Boolean =
         (localStartTime == null || localEndTime == null) && (startTime == null || endTime == null)
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is TimeRangeFilter) return false
+
+        if (startTime != other.startTime) return false
+        if (endTime != other.endTime) return false
+        if (localStartTime != other.localStartTime) return false
+        if (localEndTime != other.localEndTime) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = 0
+        result = 31 * result + (startTime?.hashCode() ?: 0)
+        result = 31 * result + (endTime?.hashCode() ?: 0)
+        result = 31 * result + (localStartTime?.hashCode() ?: 0)
+        result = 31 * result + (localEndTime?.hashCode() ?: 0)
+        return result
+    }
 }
diff --git a/health/health-data-client/src/test/java/androidx/health/data/client/impl/converters/records/AllRecordsConverterTest.kt b/health/health-data-client/src/test/java/androidx/health/data/client/impl/converters/records/AllRecordsConverterTest.kt
index 1f8ea4c..665c57e 100644
--- a/health/health-data-client/src/test/java/androidx/health/data/client/impl/converters/records/AllRecordsConverterTest.kt
+++ b/health/health-data-client/src/test/java/androidx/health/data/client/impl/converters/records/AllRecordsConverterTest.kt
@@ -42,6 +42,7 @@
 import androidx.health.data.client.records.ElevationGained
 import androidx.health.data.client.records.FloorsClimbed
 import androidx.health.data.client.records.HeartRate
+import androidx.health.data.client.records.HeartRateSeries
 import androidx.health.data.client.records.HeartRateVariabilityDifferentialIndex
 import androidx.health.data.client.records.HeartRateVariabilityRmssd
 import androidx.health.data.client.records.HeartRateVariabilityS
@@ -272,13 +273,29 @@
     }
 
     @Test
-    fun testHeartRate() {
+    fun testHeartRateSeries() {
         val data =
-            HeartRate(
-                beatsPerMinute = 1,
-                time = START_TIME,
-                zoneOffset = END_ZONE_OFFSET,
-                metadata = TEST_METADATA
+            HeartRateSeries(
+                startTime = START_TIME,
+                startZoneOffset = START_ZONE_OFFSET,
+                endTime = END_TIME,
+                endZoneOffset = END_ZONE_OFFSET,
+                samples =
+                    listOf(
+                        HeartRate(
+                            time = START_TIME,
+                            beatsPerMinute = 100L,
+                        ),
+                        HeartRate(
+                            time = START_TIME,
+                            beatsPerMinute = 110L,
+                        ),
+                        HeartRate(
+                            time = START_TIME,
+                            beatsPerMinute = 120L,
+                        ),
+                    ),
+                metadata = TEST_METADATA,
             )
 
         assertThat(toRecord(data.toProto())).isEqualTo(data)
diff --git a/health/health-data-client/src/test/java/androidx/health/data/client/time/TimeRangeFilterTest.kt b/health/health-data-client/src/test/java/androidx/health/data/client/time/TimeRangeFilterTest.kt
index 893e039..353f050 100644
--- a/health/health-data-client/src/test/java/androidx/health/data/client/time/TimeRangeFilterTest.kt
+++ b/health/health-data-client/src/test/java/androidx/health/data/client/time/TimeRangeFilterTest.kt
@@ -18,7 +18,9 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import java.time.Instant
 import java.time.LocalDateTime
+import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
+import kotlin.test.assertNotEquals
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -64,4 +66,49 @@
             endTime = LocalDateTime.parse("2021-02-01T02:00:00")
         )
     }
+
+    @Test
+    fun equals() {
+        assertEquals(
+            TimeRangeFilter.between(
+                startTime = Instant.ofEpochMilli(1234L),
+                endTime = Instant.ofEpochMilli(5679L),
+            ),
+            TimeRangeFilter.between(
+                startTime = Instant.ofEpochMilli(1234L),
+                endTime = Instant.ofEpochMilli(5679L),
+            )
+        )
+        assertEquals(
+            TimeRangeFilter.between(
+                startTime = LocalDateTime.parse("2021-02-01T01:00:00"),
+                endTime = LocalDateTime.parse("2021-02-01T02:00:00")
+            ),
+            TimeRangeFilter.between(
+                startTime = LocalDateTime.parse("2021-02-01T01:00:00"),
+                endTime = LocalDateTime.parse("2021-02-01T02:00:00")
+            )
+        )
+
+        assertNotEquals(
+            TimeRangeFilter.between(
+                startTime = Instant.ofEpochMilli(1234L),
+                endTime = Instant.ofEpochMilli(5678L),
+            ),
+            TimeRangeFilter.between(
+                startTime = Instant.ofEpochMilli(1234L),
+                endTime = Instant.ofEpochMilli(5679L),
+            )
+        )
+        assertNotEquals(
+            TimeRangeFilter.between(
+                startTime = LocalDateTime.parse("2021-02-01T01:00:00"),
+                endTime = LocalDateTime.parse("2021-02-01T02:00:00")
+            ),
+            TimeRangeFilter.between(
+                startTime = LocalDateTime.parse("2021-02-01T01:30:00"),
+                endTime = LocalDateTime.parse("2021-02-01T02:00:00")
+            )
+        )
+    }
 }
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleSupport.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleSupport.kt
index 47151f7..e2285f3 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleSupport.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandleSupport.kt
@@ -52,7 +52,7 @@
     if (savedStateRegistry.getSavedStateProvider(SAVED_STATE_KEY) == null) {
         val provider = SavedStateHandlesProvider(savedStateRegistry, this)
         savedStateRegistry.registerSavedStateProvider(SAVED_STATE_KEY, provider)
-        savedStateRegistry.runOnNextRecreation(SavedStateHandleAttacher::class.java)
+        lifecycle.addObserver(SavedStateHandleAttacher(provider))
     }
 }
 
@@ -184,11 +184,15 @@
 }
 
 // it reconnects existent SavedStateHandles to SavedStateRegistryOwner when it is recreated
-internal class SavedStateHandleAttacher : SavedStateRegistry.AutoRecreated {
-    override fun onRecreated(owner: SavedStateRegistryOwner) {
-        // if SavedStateHandlesProvider wasn't added previously, there's nothing for us to do
-        val provider = owner.savedStateRegistry
-            .getSavedStateProvider(SAVED_STATE_KEY) as? SavedStateHandlesProvider ?: return
+internal class SavedStateHandleAttacher(
+    private val provider: SavedStateHandlesProvider
+) : LifecycleEventObserver {
+
+    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+        check(event == Lifecycle.Event.ON_CREATE) {
+            "Next event must be ON_CREATE, it was $event"
+        }
+        source.lifecycle.removeObserver(this)
         // onRecreated() is called after the Lifecycle reaches CREATED, so we
         // eagerly restore the state as part of this call to ensure it consumed
         // even if no ViewModels are actually created during this cycle of the Lifecycle
diff --git a/room/room-paging-guava/build.gradle b/room/room-paging-guava/build.gradle
index 5db83cb..8f23959 100644
--- a/room/room-paging-guava/build.gradle
+++ b/room/room-paging-guava/build.gradle
@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 import androidx.build.LibraryType
 import androidx.build.Publish
 
@@ -23,9 +24,49 @@
     id("org.jetbrains.kotlin.android")
 }
 
+// If set to `true`, we'll use KSP instead of KAPT.
+// Note that the CI does not run tests with KSP yet so this is only for local usage.
+// Once variants are properly supported by both ksp and AndroidX, we'll add support for this.
+// (b/153917176)
+def useKsp = project.properties.getOrDefault("useKsp", "false").toBoolean()
+if (useKsp) {
+    apply plugin: "com.google.devtools.ksp"
+} else {
+    apply plugin: "kotlin-kapt"
+}
+
 dependencies {
     api(libs.kotlinStdlib)
-    // Add dependencies here
+    implementation(project(":room:room-paging"))
+    implementation(project(":room:room-guava"))
+    implementation(projectOrArtifact(":paging:paging-guava"))
+
+    androidTestImplementation(libs.truth)
+    androidTestImplementation(libs.testExtJunitKtx)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.kotlinTestJunit)
+    androidTestImplementation(libs.kotlinCoroutinesTest)
+    androidTestImplementation(libs.kotlinCoroutinesGuava)
+    androidTestImplementation("androidx.arch.core:core-testing:2.0.1")
+    androidTestImplementation(project(":internal-testutils-common"))
+    // depend on the shadowed version so that it tests with the shipped artifact
+    // this is a temporary attribute until KSP and AndroidX plugin supports variants.
+    if(useKsp) {
+        kspAndroidTest(
+                project(path: ":room:room-compiler", configuration: "shadowAndImplementation")
+        )
+    } else {
+        kaptAndroidTest(
+            project(path: ":room:room-compiler", configuration: "shadowAndImplementation")
+        )
+    }
+}
+
+// Allow usage of Kotlin's @OptIn.
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn"]
+    }
 }
 
 androidx {
diff --git a/room/room-paging-guava/src/androidTest/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSourceTest.kt b/room/room-paging-guava/src/androidTest/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSourceTest.kt
new file mode 100644
index 0000000..a9fd319
--- /dev/null
+++ b/room/room-paging-guava/src/androidTest/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSourceTest.kt
@@ -0,0 +1,886 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.paging.guava
+
+import android.database.Cursor
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule
+import androidx.paging.LoadType
+import androidx.paging.PagingConfig
+import androidx.paging.PagingSource
+import androidx.paging.PagingSource.LoadResult
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.Insert
+import androidx.room.PrimaryKey
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.sqlite.db.SimpleSQLiteQuery
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.testutils.TestExecutor
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import com.google.common.util.concurrent.FutureCallback
+import com.google.common.util.concurrent.Futures.addCallback
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.LinkedList
+import java.util.concurrent.CancellationException
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.guava.await
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val tableName: String = "TestItem"
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class LimitOffsetListenableFuturePagingSourceTest {
+
+    @JvmField
+    @Rule
+    val countingTaskExecutorRule = CountingTaskExecutorRule()
+
+    @Test
+    fun initialEmptyLoad_futureIsDone() = setupAndRun { db ->
+        val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+
+        runTest {
+            val listenableFuture = pagingSource.refresh()
+            val page = listenableFuture.await() as LoadResult.Page
+
+            assertThat(page.data).isEmpty()
+            assertTrue(listenableFuture.isDone)
+        }
+    }
+
+    @Test
+    fun initialLoad_returnsFutureImmediately() =
+        setupAndRunWithTestExecutor { db, _, transactionExecutor ->
+            val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+
+            val listenableFuture = pagingSource.refresh()
+            // ensure future is returned even as its result is still pending
+            assertFalse(listenableFuture.isDone)
+            assertThat(pagingSource.itemCount.get()).isEqualTo(-1)
+
+            // now execute db queries
+            transactionExecutor.executeAll() // initial transactional refresh load
+
+            val page = listenableFuture.await() as LoadResult.Page
+            assertThat(page.data).containsExactlyElementsIn(
+                ITEMS_LIST.subList(0, 15)
+            )
+            assertTrue(listenableFuture.isDone)
+        }
+
+    @Test
+    fun append_returnsFutureImmediately() =
+        setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+            val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+            pagingSource.itemCount.set(100) // bypass check for initial load
+
+            val listenableFuture = pagingSource.append(key = 20)
+            // ensure future is returned even as its result is still pending
+            assertFalse(listenableFuture.isDone)
+
+            // load append
+            queryExecutor.executeNext()
+
+            val page = listenableFuture.await() as LoadResult.Page
+            assertThat(page.data).containsExactlyElementsIn(
+                ITEMS_LIST.subList(20, 25)
+            )
+            assertTrue(listenableFuture.isDone)
+        }
+
+    @Test
+    fun prepend_returnsFutureImmediately() =
+        setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+            val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+            pagingSource.itemCount.set(100) // bypass check for initial load
+
+            val listenableFuture = pagingSource.prepend(key = 20)
+            // ensure future is returned even as its result is still pending
+            assertFalse(listenableFuture.isDone)
+
+            // load prepend
+            queryExecutor.executeNext()
+
+            val page = listenableFuture.await() as LoadResult.Page
+            assertThat(page.data).containsExactlyElementsIn(
+                ITEMS_LIST.subList(15, 20)
+            )
+            assertTrue(listenableFuture.isDone)
+        }
+
+    @Test
+    fun append_returnsInvalid() =
+        setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+            val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+            pagingSource.itemCount.set(100) // bypass check for initial load
+
+            val listenableFuture = pagingSource.append(key = 50)
+
+            pagingSource.invalidate() // imitate refreshVersionsAsync invalidating the PagingSource
+            assertTrue(pagingSource.invalid)
+
+            // executing the load Callable
+            queryExecutor.executeNext()
+
+            val result = listenableFuture.await()
+            assertThat(result).isInstanceOf(LoadResult.Invalid::class.java)
+            assertTrue(listenableFuture.isDone)
+        }
+
+    @Test
+    fun prepend_returnsInvalid() =
+        setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+            val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+            pagingSource.itemCount.set(100) // bypass check for initial load
+
+            val listenableFuture = pagingSource.prepend(key = 50)
+
+            pagingSource.invalidate() // imitate refreshVersionsAsync invalidating the PagingSource
+            assertTrue(pagingSource.invalid)
+
+            // executing the load Callable
+            queryExecutor.executeNext()
+
+            val result = listenableFuture.await()
+            assertThat(result).isInstanceOf(LoadResult.Invalid::class.java)
+            assertTrue(listenableFuture.isDone)
+        }
+
+    @Test
+    fun refresh_consecutively() = setupAndRun { db ->
+        db.dao.addAllItems(ITEMS_LIST)
+        val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+        val pagingSource2 = LimitOffsetListenableFuturePagingSourceImpl(db)
+
+        val listenableFuture1 = pagingSource.refresh(key = 10)
+        val listenableFuture2 = pagingSource2.refresh(key = 15)
+
+        // check that first Future completes first. If the first future didn't complete first,
+        // this await() would not return.
+        val page1 = listenableFuture1.await() as LoadResult.Page
+        assertThat(page1.data).containsExactlyElementsIn(
+            ITEMS_LIST.subList(10, 25)
+        )
+
+        val page2 = listenableFuture2.await() as LoadResult.Page
+        assertThat(page2.data).containsExactlyElementsIn(
+            ITEMS_LIST.subList(15, 30)
+        )
+    }
+
+    @Test
+    fun append_consecutively() =
+        setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+            val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+            pagingSource.itemCount.set(100) // bypass check for initial load
+
+            assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+
+            val listenableFuture1 = pagingSource.append(key = 10)
+            val listenableFuture2 = pagingSource.append(key = 15)
+
+            // both appends should be queued
+            assertThat(queryExecutor.queuedSize()).isEqualTo(2)
+
+            // run next append in queue and make sure it is the first append
+            queryExecutor.executeNext()
+            val page1 = listenableFuture1.await() as LoadResult.Page
+            assertThat(page1.data).containsExactlyElementsIn(
+                ITEMS_LIST.subList(10, 15)
+            )
+
+            // now run the second append
+            queryExecutor.executeNext()
+            val page2 = listenableFuture2.await() as LoadResult.Page
+            assertThat(page2.data).containsExactlyElementsIn(
+                ITEMS_LIST.subList(15, 20)
+            )
+        }
+
+    @Test
+    fun prepend_consecutively() =
+        setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+            val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+            pagingSource.itemCount.set(100) // bypass check for initial load
+
+            assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+
+            val listenableFuture1 = pagingSource.prepend(key = 30)
+            val listenableFuture2 = pagingSource.prepend(key = 25)
+
+            // both prepends should be queued
+            assertThat(queryExecutor.queuedSize()).isEqualTo(2)
+
+            // run next prepend in queue and make sure it is the first prepend
+            queryExecutor.executeNext()
+            val page1 = listenableFuture1.await() as LoadResult.Page
+            assertThat(page1.data).containsExactlyElementsIn(
+                ITEMS_LIST.subList(25, 30)
+            )
+
+            // now run the second prepend
+            queryExecutor.executeNext()
+            val page2 = listenableFuture2.await() as LoadResult.Page
+            assertThat(page2.data).containsExactlyElementsIn(
+                ITEMS_LIST.subList(20, 25)
+            )
+        }
+
+    @Test
+    fun refresh_onSuccess() = setupAndRun { db ->
+        db.dao.addAllItems(ITEMS_LIST)
+        val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+
+        val listenableFuture = pagingSource.refresh(key = 30)
+
+        var onSuccessReceived = false
+        val callbackExecutor = TestExecutor()
+        listenableFuture.onSuccess(callbackExecutor) { result ->
+            val page = result as LoadResult.Page
+            assertThat(page.data).containsExactlyElementsIn(
+                ITEMS_LIST.subList(30, 45)
+            )
+            onSuccessReceived = true
+        }
+
+        // wait until Room db's refresh load is complete
+        countingTaskExecutorRule.drainTasks(500, TimeUnit.MILLISECONDS)
+        assertTrue(listenableFuture.isDone)
+
+        callbackExecutor.executeAll()
+
+        // make sure onSuccess callback was executed
+        assertTrue(onSuccessReceived)
+        assertTrue(listenableFuture.isDone)
+    }
+
+    @Test
+    fun append_onSuccess() = setupAndRun { db ->
+        db.dao.addAllItems(ITEMS_LIST)
+        val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+        pagingSource.itemCount.set(100) // bypass check for initial load
+
+        val listenableFuture = pagingSource.append(key = 20)
+        // ensure future is returned even as its result is still pending
+        assertFalse(listenableFuture.isDone)
+
+        var onSuccessReceived = false
+        val callbackExecutor = TestExecutor()
+        listenableFuture.onSuccess(callbackExecutor) { result ->
+            val page = result as LoadResult.Page
+            assertThat(page.data).containsExactlyElementsIn(
+                ITEMS_LIST.subList(20, 25)
+            )
+            onSuccessReceived = true
+        }
+        // let room db complete load
+        countingTaskExecutorRule.drainTasks(500, TimeUnit.MILLISECONDS)
+        callbackExecutor.executeAll()
+
+        // make sure onSuccess callback was executed
+        assertTrue(onSuccessReceived)
+        assertTrue(listenableFuture.isDone)
+        }
+
+    @Test
+    fun prepend_onSuccess() = setupAndRun { db ->
+        db.dao.addAllItems(ITEMS_LIST)
+        val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+        pagingSource.itemCount.set(100) // bypass check for initial load
+
+        val listenableFuture = pagingSource.prepend(key = 40)
+        // ensure future is returned even as its result is still pending
+        assertFalse(listenableFuture.isDone)
+
+        var onSuccessReceived = false
+        val callbackExecutor = TestExecutor()
+        listenableFuture.onSuccess(callbackExecutor) { result ->
+            val page = result as LoadResult.Page
+            assertThat(page.data).containsExactlyElementsIn(
+                ITEMS_LIST.subList(35, 40)
+            )
+            onSuccessReceived = true
+        }
+        // let room db complete load
+        countingTaskExecutorRule.drainTasks(500, TimeUnit.MILLISECONDS)
+        callbackExecutor.executeAll()
+
+        // make sure onSuccess callback was executed
+        assertTrue(onSuccessReceived)
+        assertTrue(listenableFuture.isDone)
+    }
+
+    @Test
+    fun refresh_awaitThrowsCancellationException() =
+        setupAndRunWithTestExecutor { db, queryExecutor, transactionExecutor ->
+            val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+
+            val listenableFuture = pagingSource.refresh(key = 50)
+            // the initial runInTransaction load
+            assertThat(transactionExecutor.queuedSize()).isEqualTo(1)
+
+            listenableFuture.cancel(true)
+
+            assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+            assertThat(transactionExecutor.queuedSize()).isEqualTo(1)
+
+            transactionExecutor.executeNext() // initial load
+            queryExecutor.executeNext() // refreshVersionsAsync from the end runInTransaction
+
+            // await() should throw after cancellation
+            assertFailsWith<CancellationException> {
+                listenableFuture.await()
+            }
+
+            // executors should be idle
+            assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+            assertThat(transactionExecutor.queuedSize()).isEqualTo(0)
+            assertTrue(listenableFuture.isDone)
+            // even though initial refresh load is cancelled, the paging source itself
+            // is NOT invalidated
+            assertFalse(pagingSource.invalid)
+        }
+
+    @Test
+    fun append_awaitThrowsCancellationException() =
+        setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+            val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+            pagingSource.itemCount.set(100) // bypass check for initial load
+
+            // queue up the append first
+            val listenableFuture = pagingSource.append(key = 20)
+            assertThat(queryExecutor.queuedSize()).isEqualTo(1)
+
+            listenableFuture.cancel(true)
+            queryExecutor.executeNext()
+
+            // await() should throw after cancellation
+            assertFailsWith<CancellationException> {
+                listenableFuture.await()
+            }
+
+            // although query was executed, it should not complete due to the cancellation signal.
+            // If query was completed, paging source would call refreshVersionsAsync manually
+            // and queuedSize() would be 1 instead of 0 with InvalidationTracker queued up
+            assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+        }
+
+    @Test
+    fun prepend_awaitThrowsCancellationException() =
+        setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+            val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+            pagingSource.itemCount.set(100) // bypass check for initial load
+
+            // queue up the prepend first
+            val listenableFuture = pagingSource.prepend(key = 30)
+            assertThat(queryExecutor.queuedSize()).isEqualTo(1)
+
+            listenableFuture.cancel(true)
+            queryExecutor.executeNext()
+
+            // await() should throw after cancellation
+            assertFailsWith<CancellationException> {
+                listenableFuture.await()
+            }
+
+            // although query was executed, it should not complete due to the cancellation signal.
+            // If query was completed, paging source would call refreshVersionsAsync manually
+            // and queuedSize() would be 1 instead of 0 with InvalidationTracker queued up
+            assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+        }
+
+    @Test
+    fun refresh_canceledFutureRunsOnFailureCallback() =
+        setupAndRunWithTestExecutor { db, _, transactionExecutor ->
+            val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+
+            val listenableFuture = pagingSource.refresh(key = 30)
+            assertThat(transactionExecutor.queuedSize()).isEqualTo(1)
+
+            val callbackExecutor = TestExecutor()
+            var onFailureReceived = false
+            listenableFuture.onFailure(callbackExecutor) { throwable ->
+                assertThat(throwable).isInstanceOf(CancellationException::class.java)
+                onFailureReceived = true
+            }
+
+            // now cancel future and execute the refresh load. The refresh should not complete.
+            listenableFuture.cancel(true)
+            transactionExecutor.executeNext()
+            assertThat(transactionExecutor.queuedSize()).isEqualTo(0)
+
+            callbackExecutor.executeAll()
+
+            // make sure onFailure callback was executed
+            assertTrue(onFailureReceived)
+            assertTrue(listenableFuture.isDone)
+        }
+
+    @Test
+    fun append_canceledFutureRunsOnFailureCallback() =
+        setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+            val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+            pagingSource.itemCount.set(100) // bypass check for initial load
+
+            // queue up the append first
+            val listenableFuture = pagingSource.append(key = 20)
+            assertThat(queryExecutor.queuedSize()).isEqualTo(1)
+
+            val callbackExecutor = TestExecutor()
+            var onFailureReceived = false
+            listenableFuture.onFailure(callbackExecutor) { throwable ->
+                assertThat(throwable).isInstanceOf(CancellationException::class.java)
+                onFailureReceived = true
+            }
+
+            // now cancel future and execute the append load. The append should not complete.
+            listenableFuture.cancel(true)
+            queryExecutor.executeNext()
+            // if load was erroneously completed, InvalidationTracker would be queued
+            assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+
+            callbackExecutor.executeAll()
+
+            // make sure onFailure callback was executed
+            assertTrue(onFailureReceived)
+            assertTrue(listenableFuture.isDone)
+    }
+
+    @Test
+    fun prepend_canceledFutureRunsOnFailureCallback() =
+        setupAndRunWithTestExecutor { db, queryExecutor, _ ->
+            val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+            pagingSource.itemCount.set(100) // bypass check for initial load
+
+            // queue up the prepend first
+            val listenableFuture = pagingSource.prepend(key = 30)
+            assertThat(queryExecutor.queuedSize()).isEqualTo(1)
+
+            val callbackExecutor = TestExecutor()
+            var onFailureReceived = false
+            listenableFuture.onFailure(callbackExecutor) { throwable ->
+                assertThat(throwable).isInstanceOf(CancellationException::class.java)
+                onFailureReceived = true
+            }
+
+            // now cancel future and execute the prepend which should not complete.
+            listenableFuture.cancel(true)
+            queryExecutor.executeNext()
+            // if load was erroneously completed, InvalidationTracker would be queued
+            assertThat(queryExecutor.queuedSize()).isEqualTo(0)
+
+            callbackExecutor.executeAll()
+
+            // make sure onFailure callback was executed
+            assertTrue(onFailureReceived)
+            assertTrue(listenableFuture.isDone)
+        }
+
+    @Test
+    fun refresh_AfterCancellation() = setupAndRun { db ->
+        db.dao.addAllItems(ITEMS_LIST)
+        val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+        pagingSource.itemCount.set(100) // bypass check for initial load
+
+        val listenableFuture = pagingSource.prepend(key = 50)
+
+        listenableFuture.cancel(true)
+        assertFailsWith<CancellationException> {
+            listenableFuture.await()
+        }
+
+        // new gen after query from previous gen was cancelled
+        val pagingSource2 = LimitOffsetListenableFuturePagingSourceImpl(db)
+        val listenableFuture2 = pagingSource2.refresh()
+        val result = listenableFuture2.await() as LoadResult.Page
+
+        // the new generation should load as usual
+        assertThat(result.data).containsExactlyElementsIn(
+            ITEMS_LIST.subList(0, 15)
+        )
+    }
+
+    @Test
+    fun appendAgain_afterFutureCanceled() = setupAndRun { db ->
+        db.dao.addAllItems(ITEMS_LIST)
+        val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+        pagingSource.itemCount.set(100) // bypass check for initial load
+
+        val listenableFuture = pagingSource.append(key = 30)
+
+        listenableFuture.cancel(true)
+        assertFailsWith<CancellationException> {
+            listenableFuture.await()
+        }
+        assertTrue(listenableFuture.isDone)
+        assertFalse(pagingSource.invalid)
+
+        val listenableFuture2 = pagingSource.append(key = 30)
+
+        val result = listenableFuture2.await() as LoadResult.Page
+        assertThat(result.data).containsExactlyElementsIn(
+            ITEMS_LIST.subList(30, 35)
+        )
+        assertTrue(listenableFuture2.isDone)
+    }
+
+    @Test
+    fun prependAgain_afterFutureCanceled() = setupAndRun { db ->
+        db.dao.addAllItems(ITEMS_LIST)
+        val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+        pagingSource.itemCount.set(100) // bypass check for initial load
+
+        val listenableFuture = pagingSource.prepend(key = 30)
+
+        listenableFuture.cancel(true)
+        assertFailsWith<CancellationException> {
+            listenableFuture.await()
+        }
+        assertFalse(pagingSource.invalid)
+        assertTrue(listenableFuture.isDone)
+
+        val listenableFuture2 = pagingSource.prepend(key = 30)
+
+        val result = listenableFuture2.await() as LoadResult.Page
+            assertThat(result.data).containsExactlyElementsIn(
+                ITEMS_LIST.subList(25, 30)
+            )
+        assertTrue(listenableFuture2.isDone)
+    }
+
+    @Test
+    fun test_jumpSupport() = setupAndRun { db ->
+        val pagingSource = LimitOffsetListenableFuturePagingSourceImpl(db)
+        assertTrue(pagingSource.jumpingSupported)
+    }
+
+    @Test
+    fun refresh_secondaryConstructor() = setupAndRun { db ->
+        val pagingSource = object : LimitOffsetListenableFuturePagingSource<TestItem>(
+            db = db,
+            supportSQLiteQuery = SimpleSQLiteQuery(
+                "SELECT * FROM $tableName ORDER BY id ASC"
+            )
+        ) {
+            override fun convertRows(cursor: Cursor): List<TestItem> {
+                return convertRowsHelper(cursor)
+            }
+        }
+
+        db.dao.addAllItems(ITEMS_LIST)
+        val listenableFuture = pagingSource.refresh()
+
+        val page = listenableFuture.await() as LoadResult.Page
+        assertThat(page.data).containsExactlyElementsIn(
+            ITEMS_LIST.subList(0, 15)
+        )
+        assertTrue(listenableFuture.isDone)
+    }
+
+    @Test
+    fun append_secondaryConstructor() = setupAndRun { db ->
+        val pagingSource = object : LimitOffsetListenableFuturePagingSource<TestItem>(
+            db = db,
+            supportSQLiteQuery = SimpleSQLiteQuery(
+                "SELECT * FROM $tableName ORDER BY id ASC"
+            )
+        ) {
+            override fun convertRows(cursor: Cursor): List<TestItem> {
+                return convertRowsHelper(cursor)
+            }
+        }
+
+        db.dao.addAllItems(ITEMS_LIST)
+        pagingSource.itemCount.set(100)
+        val listenableFuture = pagingSource.append(key = 50)
+
+        val page = listenableFuture.await() as LoadResult.Page
+        assertThat(page.data).containsExactlyElementsIn(
+            ITEMS_LIST.subList(50, 55)
+        )
+        assertTrue(listenableFuture.isDone)
+    }
+
+    @Test
+    fun prepend_secondaryConstructor() = setupAndRun { db ->
+        val pagingSource = object : LimitOffsetListenableFuturePagingSource<TestItem>(
+            db = db,
+            supportSQLiteQuery = SimpleSQLiteQuery(
+                "SELECT * FROM $tableName ORDER BY id ASC"
+            )
+        ) {
+            override fun convertRows(cursor: Cursor): List<TestItem> {
+                return convertRowsHelper(cursor)
+            }
+        }
+
+        db.dao.addAllItems(ITEMS_LIST)
+        pagingSource.itemCount.set(100)
+        val listenableFuture = pagingSource.prepend(key = 50)
+
+        val page = listenableFuture.await() as LoadResult.Page
+        assertThat(page.data).containsExactlyElementsIn(
+            ITEMS_LIST.subList(45, 50)
+        )
+        assertTrue(listenableFuture.isDone)
+    }
+
+    private fun setupAndRun(
+        test: suspend (LimitOffsetTestDb) -> Unit
+    ) {
+        val db = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(),
+            LimitOffsetTestDb::class.java
+        ).build()
+
+        runTest {
+            test(db)
+        }
+        tearDown(db)
+    }
+
+    private fun setupAndRunWithTestExecutor(
+        test: suspend (LimitOffsetTestDb, TestExecutor, TestExecutor) -> Unit
+    ) {
+        val queryExecutor = TestExecutor()
+        val transactionExecutor = TestExecutor()
+        val db = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(),
+            LimitOffsetTestDb::class.java
+        )
+            .setTransactionExecutor(transactionExecutor)
+            .setQueryExecutor(queryExecutor)
+            .build()
+
+        runTest {
+            db.dao.addAllItems(ITEMS_LIST)
+            queryExecutor.executeNext() // InvalidationTracker from the addAllItems
+          test(db, queryExecutor, transactionExecutor)
+        }
+        tearDown(db)
+    }
+
+    private fun tearDown(db: LimitOffsetTestDb) {
+        if (db.isOpen) db.close()
+        countingTaskExecutorRule.drainTasks(500, TimeUnit.MILLISECONDS)
+        assertThat(countingTaskExecutorRule.isIdle).isTrue()
+    }
+}
+
+private class LimitOffsetListenableFuturePagingSourceImpl(
+    db: RoomDatabase,
+    queryString: String = "SELECT * FROM $tableName ORDER BY id ASC",
+) : LimitOffsetListenableFuturePagingSource<TestItem>(
+    sourceQuery = RoomSQLiteQuery.acquire(
+        queryString,
+        0
+    ),
+    db = db,
+    tables = arrayOf(tableName)
+) {
+    override fun convertRows(cursor: Cursor): List<TestItem> {
+        return convertRowsHelper(cursor)
+    }
+}
+
+private fun convertRowsHelper(cursor: Cursor): List<TestItem> {
+    val cursorIndexOfId = getColumnIndexOrThrow(cursor, "id")
+    val data = mutableListOf<TestItem>()
+    while (cursor.moveToNext()) {
+        val tmpId = cursor.getInt(cursorIndexOfId)
+        data.add(TestItem(tmpId))
+    }
+    return data
+}
+
+@Suppress("UNCHECKED_CAST")
+private fun TestExecutor.executeNext() {
+    val tasks = javaClass.getDeclaredField("mTasks").let {
+        it.isAccessible = true
+        it.get(this)
+    } as LinkedList<Runnable>
+
+    if (!tasks.isEmpty()) {
+        val task = tasks.poll()
+        task?.run()
+    }
+}
+
+@Suppress("UNCHECKED_CAST")
+private fun TestExecutor.queuedSize(): Int {
+    val tasks = javaClass.getDeclaredField("mTasks").let {
+        it.isAccessible = true
+        it.get(this)
+    } as LinkedList<Runnable>
+
+    return tasks.size
+}
+
+private fun LimitOffsetListenableFuturePagingSource<TestItem>.refresh(
+    key: Int? = null,
+): ListenableFuture<LoadResult<Int, TestItem>> {
+    return loadFuture(
+        createLoadParam(
+            loadType = LoadType.REFRESH,
+            key = key,
+        )
+    )
+}
+
+private fun LimitOffsetListenableFuturePagingSource<TestItem>.append(
+    key: Int? = -1,
+): ListenableFuture<LoadResult<Int, TestItem>> {
+    return loadFuture(
+        createLoadParam(
+            loadType = LoadType.APPEND,
+            key = key,
+        )
+    )
+}
+
+private fun LimitOffsetListenableFuturePagingSource<TestItem>.prepend(
+    key: Int? = -1,
+): ListenableFuture<LoadResult<Int, TestItem>> {
+    return loadFuture(
+        createLoadParam(
+            loadType = LoadType.PREPEND,
+            key = key,
+        )
+    )
+}
+
+private val CONFIG = PagingConfig(
+    pageSize = 5,
+    enablePlaceholders = true,
+    initialLoadSize = 15
+)
+
+private val ITEMS_LIST = createItemsForDb(0, 100)
+
+private fun createItemsForDb(startId: Int, count: Int): List<TestItem> {
+    return List(count) {
+        TestItem(
+            id = it + startId,
+        )
+    }
+}
+
+private fun createLoadParam(
+    loadType: LoadType,
+    key: Int? = null,
+    initialLoadSize: Int = CONFIG.initialLoadSize,
+    pageSize: Int = CONFIG.pageSize,
+    placeholdersEnabled: Boolean = CONFIG.enablePlaceholders
+): PagingSource.LoadParams<Int> {
+    return when (loadType) {
+        LoadType.REFRESH -> {
+            PagingSource.LoadParams.Refresh(
+                key = key,
+                loadSize = initialLoadSize,
+                placeholdersEnabled = placeholdersEnabled
+            )
+        }
+        LoadType.APPEND -> {
+            PagingSource.LoadParams.Append(
+                key = key ?: -1,
+                loadSize = pageSize,
+                placeholdersEnabled = placeholdersEnabled
+            )
+        }
+        LoadType.PREPEND -> {
+            PagingSource.LoadParams.Prepend(
+                key = key ?: -1,
+                loadSize = pageSize,
+                placeholdersEnabled = placeholdersEnabled
+            )
+        }
+    }
+}
+
+private fun ListenableFuture<LoadResult<Int, TestItem>>.onSuccess(
+    executor: Executor,
+    onSuccessCallback: (LoadResult<Int, TestItem>?) -> Unit,
+) {
+    addCallback(
+        this,
+        object : FutureCallback<LoadResult<Int, TestItem>> {
+            override fun onSuccess(result: LoadResult<Int, TestItem>?) {
+                onSuccessCallback(result)
+            }
+
+            override fun onFailure(t: Throwable) {
+                assertWithMessage("Expected onSuccess callback instead of onFailure, " +
+                    "received ${t.localizedMessage}").fail()
+            }
+        },
+        executor
+    )
+}
+
+private fun ListenableFuture<LoadResult<Int, TestItem>>.onFailure(
+    executor: Executor,
+    onFailureCallback: (Throwable) -> Unit,
+) {
+    addCallback(
+        this,
+        object : FutureCallback<LoadResult<Int, TestItem>> {
+            override fun onSuccess(result: LoadResult<Int, TestItem>?) {
+                assertWithMessage("Expected onFailure callback instead of onSuccess, " +
+                    "received result $result").fail()
+            }
+
+            override fun onFailure(t: Throwable) {
+                onFailureCallback(t)
+            }
+        },
+        executor
+    )
+}
+
+@Database(entities = [TestItem::class], version = 1, exportSchema = false)
+abstract class LimitOffsetTestDb : RoomDatabase() {
+    abstract val dao: TestItemDao
+}
+
+@Entity(tableName = "TestItem")
+data class TestItem(
+    @PrimaryKey val id: Int,
+    val value: String = "item $id"
+)
+
+@Dao
+interface TestItemDao {
+    @Insert
+    fun addAllItems(testItems: List<TestItem>)
+}
diff --git a/room/room-paging-guava/src/main/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSource.kt b/room/room-paging-guava/src/main/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSource.kt
new file mode 100644
index 0000000..fb2d209
--- /dev/null
+++ b/room/room-paging-guava/src/main/kotlin/androidx/room/paging/guava/LimitOffsetListenableFuturePagingSource.kt
@@ -0,0 +1,151 @@
+ /*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.paging.guava
+
+import android.database.Cursor
+import androidx.annotation.NonNull
+import androidx.annotation.RestrictTo
+import androidx.paging.ListenableFuturePagingSource
+import androidx.paging.PagingState
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.guava.GuavaRoom.createListenableFuture
+import androidx.room.paging.util.INITIAL_ITEM_COUNT
+import androidx.room.paging.util.INVALID
+import androidx.room.paging.util.ThreadSafeInvalidationObserver
+import androidx.room.paging.util.getClippedRefreshKey
+import androidx.room.paging.util.queryDatabase
+import androidx.room.paging.util.queryItemCount
+import androidx.room.util.createCancellationSignal
+import androidx.sqlite.db.SupportSQLiteQuery
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.Callable
+import java.util.concurrent.atomic.AtomicInteger
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+abstract class LimitOffsetListenableFuturePagingSource<Value : Any>(
+    private val sourceQuery: RoomSQLiteQuery,
+    private val db: RoomDatabase,
+    vararg tables: String
+) : ListenableFuturePagingSource<Int, Value>() {
+
+    constructor(
+        supportSQLiteQuery: SupportSQLiteQuery,
+        db: RoomDatabase,
+        vararg tables: String,
+    ) : this(
+        sourceQuery = RoomSQLiteQuery.copyFrom(supportSQLiteQuery),
+        db = db,
+        tables = tables,
+    )
+
+    // internal for testing visibility
+    internal val itemCount: AtomicInteger = AtomicInteger(INITIAL_ITEM_COUNT)
+    private val observer = ThreadSafeInvalidationObserver(tables = tables, ::invalidate)
+
+    /**
+    * Returns a [ListenableFuture] immediately before loading from the database completes
+    *
+    * If PagingSource is invalidated while the [ListenableFuture] is still pending, the
+    * invalidation will cancel the load() coroutine that calls await() on this future. The
+    * cancellation of await() will transitively cancel this future as well.
+    */
+    override fun loadFuture(params: LoadParams<Int>): ListenableFuture<LoadResult<Int, Value>> {
+        observer.registerIfNecessary(db)
+        val tempCount = itemCount.get()
+        return if (tempCount == INITIAL_ITEM_COUNT) {
+            initialLoad(params)
+        } else {
+            nonInitialLoad(params, tempCount)
+        }
+    }
+
+    /**
+    * For refresh loads
+    *
+    * To guarantee a valid initial load, it is run in transaction so that db writes cannot
+    * happen in between [queryItemCount] and [queryDatabase] to ensure a valid [itemCount].
+    * [itemCount] must be correct in order to calculate correct LIMIT and OFFSET for the query.
+    *
+    *
+    * However, the database load will be canceled via the cancellation signal if the future
+    * it returned has been canceled before it has completed.
+    */
+    private fun initialLoad(params: LoadParams<Int>): ListenableFuture<LoadResult<Int, Value>> {
+        val cancellationSignal = createCancellationSignal()
+        val loadCallable = Callable<LoadResult<Int, Value>> {
+            db.runInTransaction(
+                Callable {
+                    val tempCount = queryItemCount(sourceQuery, db)
+                    itemCount.set(tempCount)
+                    queryDatabase(
+                        params, sourceQuery, db, tempCount, cancellationSignal, ::convertRows
+                    )
+                }
+            )
+        }
+
+        return createListenableFuture(
+            db,
+            true,
+            loadCallable,
+            sourceQuery,
+            false,
+            cancellationSignal,
+        )
+    }
+
+    /**
+    * For append and prepend loads
+    *
+    * The cancellation signal cancels room database operation if its running, or cancels it
+    * the moment it starts. The signal is triggered when the future is canceled.
+    */
+    private fun nonInitialLoad(
+        params: LoadParams<Int>,
+        tempCount: Int
+    ): ListenableFuture<LoadResult<Int, Value>> {
+        val cancellationSignal = createCancellationSignal()
+        val loadCallable = Callable<LoadResult<Int, Value>> {
+            val result = queryDatabase(
+                params, sourceQuery, db, tempCount, cancellationSignal, ::convertRows
+            )
+            db.invalidationTracker.refreshVersionsAsync()
+            @Suppress("UNCHECKED_CAST")
+            if (invalid) INVALID as LoadResult.Invalid<Int, Value> else result
+        }
+
+        return createListenableFuture(
+            db,
+            false,
+            loadCallable,
+            sourceQuery,
+            false,
+            cancellationSignal
+        )
+    }
+
+    @NonNull
+    protected abstract fun convertRows(cursor: Cursor): List<Value>
+
+    override val jumpingSupported: Boolean
+        get() = true
+
+    override fun getRefreshKey(state: PagingState<Int, Value>): Int? {
+         return state.getClippedRefreshKey()
+    }
+}
\ No newline at end of file
diff --git a/room/room-paging/build.gradle b/room/room-paging/build.gradle
index 8e6600f..84367bf 100644
--- a/room/room-paging/build.gradle
+++ b/room/room-paging/build.gradle
@@ -45,7 +45,7 @@
 dependencies {
     api(libs.kotlinStdlib)
 
-    implementation(project(":room:room-runtime"))
+    api(project(":room:room-runtime"))
     implementation(project(":room:room-ktx"))
     api("androidx.paging:paging-common:3.1.0")
 
diff --git a/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt b/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
index 023e5b6..6634131 100644
--- a/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
+++ b/room/room-paging/src/main/kotlin/androidx/room/paging/LimitOffsetPagingSource.kt
@@ -24,6 +24,8 @@
 import androidx.room.RoomDatabase
 import androidx.room.RoomSQLiteQuery
 import androidx.room.getQueryDispatcher
+import androidx.room.paging.util.INITIAL_ITEM_COUNT
+import androidx.room.paging.util.INVALID
 import androidx.room.paging.util.ThreadSafeInvalidationObserver
 import androidx.room.paging.util.getClippedRefreshKey
 import androidx.room.paging.util.queryDatabase
@@ -40,9 +42,6 @@
  * for Pager's consumption. Registers observers on tables lazily and automatically invalidates
  * itself when data changes.
  */
-
-private val INVALID = PagingSource.LoadResult.Invalid<Any, Any>()
-
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 abstract class LimitOffsetPagingSource<Value : Any>(
     private val sourceQuery: RoomSQLiteQuery,
@@ -60,7 +59,7 @@
         tables = tables,
     )
 
-    internal val itemCount: AtomicInteger = AtomicInteger(-1)
+    internal val itemCount: AtomicInteger = AtomicInteger(INITIAL_ITEM_COUNT)
 
     private val observer = ThreadSafeInvalidationObserver(
         tables = tables,
@@ -72,7 +71,7 @@
             observer.registerIfNecessary(db)
             val tempCount = itemCount.get()
             // if itemCount is < 0, then it is initial load
-            if (tempCount < 0) {
+            if (tempCount == INITIAL_ITEM_COUNT) {
                 initialLoad(params)
             } else {
                 nonInitialLoad(params, tempCount)
@@ -94,7 +93,13 @@
         return db.withTransaction {
             val tempCount = queryItemCount(sourceQuery, db)
             itemCount.set(tempCount)
-            queryDatabase(params, sourceQuery, db, tempCount, ::convertRows)
+            queryDatabase(
+                params = params,
+                sourceQuery = sourceQuery,
+                db = db,
+                itemCount = tempCount,
+                convertRows = ::convertRows
+            )
         }
     }
 
@@ -102,8 +107,14 @@
         params: LoadParams<Int>,
         tempCount: Int,
     ): LoadResult<Int, Value> {
-        val loadResult = queryDatabase(params, sourceQuery, db, tempCount, ::convertRows)
-        // manually check if database has been updated. If so, the observers's
+        val loadResult = queryDatabase(
+            params = params,
+            sourceQuery = sourceQuery,
+            db = db,
+            itemCount = tempCount,
+            convertRows = ::convertRows
+        )
+        // manually check if database has been updated. If so, the observer's
         // invalidation callback will invalidate this paging source
         db.invalidationTracker.refreshVersionsSync()
         @Suppress("UNCHECKED_CAST")
diff --git a/room/room-paging/src/main/kotlin/androidx/room/paging/util/RoomPagingUtil.kt b/room/room-paging/src/main/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
index 2511fc6..1722c11 100644
--- a/room/room-paging/src/main/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
+++ b/room/room-paging/src/main/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
@@ -18,6 +18,7 @@
 package androidx.room.paging.util
 
 import android.database.Cursor
+import android.os.CancellationSignal
 import androidx.annotation.RestrictTo
 import androidx.paging.PagingSource
 import androidx.paging.PagingSource.LoadParams
@@ -30,6 +31,18 @@
 import androidx.room.RoomSQLiteQuery
 
 /**
+ * A [LoadResult] that can be returned to trigger a new generation of PagingSource
+ *
+ * Any loaded data or queued loads prior to returning INVALID will be discarded
+ */
+val INVALID = LoadResult.Invalid<Any, Any>()
+
+/**
+ * The default itemCount value
+ */
+const val INITIAL_ITEM_COUNT = -1
+
+/**
  * Calculates query limit based on LoadType.
  *
  * Prepend: If requested loadSize is larger than available number of items to prepend, it will
@@ -102,6 +115,8 @@
  * @param itemCount the db row count, triggers a new PagingSource generation if itemCount changes,
  * i.e. items are added / removed
  *
+ * @param cancellationSignal the signal to cancel the query if the query hasn't yet completed
+ *
  * @param convertRows the function to iterate data with provided [Cursor] to return List<Value>
  */
 fun <Value : Any> queryDatabase(
@@ -109,6 +124,7 @@
     sourceQuery: RoomSQLiteQuery,
     db: RoomDatabase,
     itemCount: Int,
+    cancellationSignal: CancellationSignal? = null,
     convertRows: (Cursor) -> List<Value>,
 ): LoadResult<Int, Value> {
     val key = params.key ?: 0
@@ -121,7 +137,7 @@
         sourceQuery.argCount
     )
     sqLiteQuery.copyArgumentsFrom(sourceQuery)
-    val cursor = db.query(sqLiteQuery)
+    val cursor = db.query(sqLiteQuery, cancellationSignal)
     val data: List<Value>
     try {
         data = convertRows(cursor)
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index 8e6728e..78a48d4 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -15,18 +15,15 @@
   }
 
   @androidx.compose.runtime.Stable public interface ArcPaddingValues {
-    method public float calculateEndPadding();
-    method public float calculateInnerPadding();
-    method public float calculateOuterPadding();
-    method public float calculateStartPadding();
+    method public float calculateAfterPadding(androidx.compose.ui.unit.LayoutDirection layoutDirection, int angularDirection);
+    method public float calculateBeforePadding(androidx.compose.ui.unit.LayoutDirection layoutDirection, int angularDirection);
+    method public float calculateInnerPadding(int radialDirection);
+    method public float calculateOuterPadding(int radialDirection);
   }
 
   public final class BasicCurvedTextKt {
-    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float outer, optional float inner, optional float start, optional float end);
-    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(float all);
-    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float radial, optional float angular);
-    method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow, optional kotlin.jvm.functions.Function0<androidx.wear.compose.foundation.CurvedTextStyle> style);
-    method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, androidx.wear.compose.foundation.CurvedTextStyle style, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow);
+    method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow, optional kotlin.jvm.functions.Function0<androidx.wear.compose.foundation.CurvedTextStyle> style);
+    method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, androidx.wear.compose.foundation.CurvedTextStyle style, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
   }
 
   public interface CurvedAlignment {
@@ -120,6 +117,16 @@
   public final class CurvedModifierKt {
   }
 
+  public final class CurvedPaddingKt {
+    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float outer, optional float inner, optional float before, optional float after);
+    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(float all);
+    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float radial, optional float angular);
+    method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, androidx.wear.compose.foundation.ArcPaddingValues paddingValues);
+    method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, float outer, float inner, float before, float after);
+    method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, optional float radial, optional float angular);
+    method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, optional float all);
+  }
+
   public final class CurvedParentDataKt {
     method public static androidx.wear.compose.foundation.CurvedModifier parentDataModifier(androidx.wear.compose.foundation.CurvedModifier, kotlin.jvm.functions.Function1<java.lang.Object,?> modifyParentData);
     method public static androidx.wear.compose.foundation.CurvedModifier weight(androidx.wear.compose.foundation.CurvedModifier, float weight);
diff --git a/wear/compose/compose-foundation/api/public_plus_experimental_current.txt b/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
index 8e6728e..78a48d4 100644
--- a/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-foundation/api/public_plus_experimental_current.txt
@@ -15,18 +15,15 @@
   }
 
   @androidx.compose.runtime.Stable public interface ArcPaddingValues {
-    method public float calculateEndPadding();
-    method public float calculateInnerPadding();
-    method public float calculateOuterPadding();
-    method public float calculateStartPadding();
+    method public float calculateAfterPadding(androidx.compose.ui.unit.LayoutDirection layoutDirection, int angularDirection);
+    method public float calculateBeforePadding(androidx.compose.ui.unit.LayoutDirection layoutDirection, int angularDirection);
+    method public float calculateInnerPadding(int radialDirection);
+    method public float calculateOuterPadding(int radialDirection);
   }
 
   public final class BasicCurvedTextKt {
-    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float outer, optional float inner, optional float start, optional float end);
-    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(float all);
-    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float radial, optional float angular);
-    method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow, optional kotlin.jvm.functions.Function0<androidx.wear.compose.foundation.CurvedTextStyle> style);
-    method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, androidx.wear.compose.foundation.CurvedTextStyle style, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow);
+    method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow, optional kotlin.jvm.functions.Function0<androidx.wear.compose.foundation.CurvedTextStyle> style);
+    method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, androidx.wear.compose.foundation.CurvedTextStyle style, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
   }
 
   public interface CurvedAlignment {
@@ -120,6 +117,16 @@
   public final class CurvedModifierKt {
   }
 
+  public final class CurvedPaddingKt {
+    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float outer, optional float inner, optional float before, optional float after);
+    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(float all);
+    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float radial, optional float angular);
+    method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, androidx.wear.compose.foundation.ArcPaddingValues paddingValues);
+    method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, float outer, float inner, float before, float after);
+    method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, optional float radial, optional float angular);
+    method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, optional float all);
+  }
+
   public final class CurvedParentDataKt {
     method public static androidx.wear.compose.foundation.CurvedModifier parentDataModifier(androidx.wear.compose.foundation.CurvedModifier, kotlin.jvm.functions.Function1<java.lang.Object,?> modifyParentData);
     method public static androidx.wear.compose.foundation.CurvedModifier weight(androidx.wear.compose.foundation.CurvedModifier, float weight);
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index 8e6728e..78a48d4 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -15,18 +15,15 @@
   }
 
   @androidx.compose.runtime.Stable public interface ArcPaddingValues {
-    method public float calculateEndPadding();
-    method public float calculateInnerPadding();
-    method public float calculateOuterPadding();
-    method public float calculateStartPadding();
+    method public float calculateAfterPadding(androidx.compose.ui.unit.LayoutDirection layoutDirection, int angularDirection);
+    method public float calculateBeforePadding(androidx.compose.ui.unit.LayoutDirection layoutDirection, int angularDirection);
+    method public float calculateInnerPadding(int radialDirection);
+    method public float calculateOuterPadding(int radialDirection);
   }
 
   public final class BasicCurvedTextKt {
-    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float outer, optional float inner, optional float start, optional float end);
-    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(float all);
-    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float radial, optional float angular);
-    method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow, optional kotlin.jvm.functions.Function0<androidx.wear.compose.foundation.CurvedTextStyle> style);
-    method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, androidx.wear.compose.foundation.CurvedTextStyle style, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow);
+    method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow, optional kotlin.jvm.functions.Function0<androidx.wear.compose.foundation.CurvedTextStyle> style);
+    method public static void basicCurvedText(androidx.wear.compose.foundation.CurvedScope, String text, androidx.wear.compose.foundation.CurvedTextStyle style, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
   }
 
   public interface CurvedAlignment {
@@ -120,6 +117,16 @@
   public final class CurvedModifierKt {
   }
 
+  public final class CurvedPaddingKt {
+    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float outer, optional float inner, optional float before, optional float after);
+    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(float all);
+    method public static androidx.wear.compose.foundation.ArcPaddingValues ArcPaddingValues(optional float radial, optional float angular);
+    method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, androidx.wear.compose.foundation.ArcPaddingValues paddingValues);
+    method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, float outer, float inner, float before, float after);
+    method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, optional float radial, optional float angular);
+    method public static androidx.wear.compose.foundation.CurvedModifier padding(androidx.wear.compose.foundation.CurvedModifier, optional float all);
+  }
+
   public final class CurvedParentDataKt {
     method public static androidx.wear.compose.foundation.CurvedModifier parentDataModifier(androidx.wear.compose.foundation.CurvedModifier, kotlin.jvm.functions.Function1<java.lang.Object,?> modifyParentData);
     method public static androidx.wear.compose.foundation.CurvedModifier weight(androidx.wear.compose.foundation.CurvedModifier, float weight);
diff --git a/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedWorldSample.kt b/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedWorldSample.kt
index 0738444..8279fb3 100644
--- a/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedWorldSample.kt
+++ b/wear/compose/compose-foundation/samples/src/main/java/androidx/wear/compose/foundation/samples/CurvedWorldSample.kt
@@ -30,7 +30,6 @@
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
-import androidx.wear.compose.foundation.ArcPaddingValues
 import androidx.wear.compose.foundation.CurvedAlignment
 import androidx.wear.compose.foundation.CurvedDirection
 import androidx.wear.compose.foundation.CurvedLayout
@@ -43,6 +42,7 @@
 import androidx.wear.compose.foundation.curvedColumn
 import androidx.wear.compose.foundation.curvedComposable
 import androidx.wear.compose.foundation.curvedRow
+import androidx.wear.compose.foundation.padding
 import androidx.wear.compose.foundation.radialGradientBackground
 import androidx.wear.compose.foundation.size
 import androidx.wear.compose.foundation.weight
@@ -125,14 +125,14 @@
     CurvedLayout(modifier = Modifier.fillMaxSize()) {
         basicCurvedText(
             "Curved Text",
+            CurvedModifier.padding(10.dp),
             style = {
                 CurvedTextStyle(
                     fontSize = 16.sp,
                     color = Color.Black,
                     background = Color.White
                 )
-            },
-            contentArcPadding = ArcPaddingValues(10.dp)
+            }
         )
         curvedComposable {
             Box(modifier = Modifier.size(20.dp).background(Color.Gray))
@@ -185,9 +185,9 @@
                     color = Color.Black
                 )
             },
-            contentArcPadding = ArcPaddingValues(5.dp),
             modifier = CurvedModifier
                 .radialGradientBackground(0f to Color.White, 1f to Color.Black)
+                .padding(5.dp)
         )
         basicCurvedText(
             "Angular",
@@ -197,9 +197,9 @@
                     color = Color.Black
                 )
             },
-            contentArcPadding = ArcPaddingValues(5.dp),
             modifier = CurvedModifier
                 .angularGradientBackground(0f to Color.White, 1f to Color.Black)
+                .padding(5.dp)
         )
     }
 }
diff --git a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/SpyModifier.kt b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/SpyModifier.kt
index 712f70c..45360e3 100644
--- a/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/SpyModifier.kt
+++ b/wear/compose/compose-foundation/src/androidAndroidTest/kotlin/androidx/wear/compose/foundation/SpyModifier.kt
@@ -19,7 +19,6 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.Placeable
 import org.junit.Assert
 
@@ -119,7 +118,7 @@
 internal class SpyCurvedChildWrapper(private val capturedInfo: CapturedInfo, wrapped: CurvedChild) :
     BaseCurvedChildWrapper(wrapped) {
 
-    override fun MeasureScope.initializeMeasure(
+    override fun CurvedMeasureScope.initializeMeasure(
         measurables: List<Measurable>,
         index: Int
     ): Int = with(wrapped) {
diff --git a/wear/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt b/wear/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt
index 2b9af94..57a9954 100644
--- a/wear/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt
+++ b/wear/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt
@@ -38,7 +38,6 @@
     private var text: String = ""
     private var clockwise: Boolean = true
     private var fontSizePx: Float = 0f
-    private var arcPaddingPx: ArcPaddingPx = ArcPaddingPx(0f, 0f, 0f, 0f)
 
     actual var textWidth by mutableStateOf(0f)
     actual var textHeight by mutableStateOf(0f)
@@ -53,19 +52,16 @@
     actual fun updateIfNeeded(
         text: String,
         clockwise: Boolean,
-        fontSizePx: Float,
-        arcPaddingPx: ArcPaddingPx
+        fontSizePx: Float
     ) {
         if (
             text != this.text ||
             clockwise != this.clockwise ||
-            fontSizePx != this.fontSizePx ||
-            arcPaddingPx != this.arcPaddingPx
+            fontSizePx != this.fontSizePx
         ) {
             this.text = text
             this.clockwise = clockwise
             this.fontSizePx = fontSizePx
-            this.arcPaddingPx = arcPaddingPx
             doUpdate()
             lastLayoutInfo = null // Ensure paths are recomputed
         }
@@ -77,10 +73,9 @@
         val rect = android.graphics.Rect()
         paint.getTextBounds(text, 0, text.length, rect)
 
-        textWidth = rect.width() + arcPaddingPx.before + arcPaddingPx.after
-        textHeight = -paint.fontMetrics.top + paint.fontMetrics.bottom +
-            arcPaddingPx.inner + arcPaddingPx.outer
-        baseLinePosition = arcPaddingPx.outer +
+        textWidth = rect.width().toFloat()
+        textHeight = -paint.fontMetrics.top + paint.fontMetrics.bottom
+        baseLinePosition =
             if (clockwise) -paint.fontMetrics.top else paint.fontMetrics.bottom
     }
 
@@ -90,9 +85,6 @@
             with(layoutInfo) {
                 val clockwiseFactor = if (clockwise) 1f else -1f
 
-                val paddingBeforeAsAngle = (arcPaddingPx.before / measureRadius)
-                    .toDegrees()
-                    .coerceAtMost(360f)
                 val sweepDegree = sweepRadians.toDegrees().coerceAtMost(360f)
 
                 val centerX = centerOffset.x
@@ -125,8 +117,7 @@
                     centerX + measureRadius,
                     centerY + measureRadius,
                     startAngleRadians.toDegrees() +
-                        (if (clockwise) paddingBeforeAsAngle
-                        else sweepDegree - paddingBeforeAsAngle),
+                        (if (clockwise) 0f else sweepDegree),
                     clockwiseFactor * sweepDegree
                 )
             }
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/BasicCurvedText.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/BasicCurvedText.kt
index 70a9ac5..ebb4b39 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/BasicCurvedText.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/BasicCurvedText.kt
@@ -17,97 +17,11 @@
 package androidx.wear.compose.foundation
 
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Stable
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-
-/**
- * Apply additional space along each edge of the content in [Dp].
- * See the [ArcPaddingValues] factories for convenient ways to
- * build [ArcPaddingValues].
- */
-@Stable
-public interface ArcPaddingValues {
-    /**
-     * Padding in the outward direction from the center of the [CurvedLayout]
-     */
-    fun calculateOuterPadding(): Dp
-
-    /**
-     * Padding in the inwards direction towards the center of the [CurvedLayout]
-     */
-    fun calculateInnerPadding(): Dp
-
-    /**
-     * Padding added at the start of the component.
-     */
-    fun calculateStartPadding(): Dp
-
-    /**
-     * Padding added at the end of the component.
-     */
-    fun calculateEndPadding(): Dp
-}
-
-/**
- * Apply additional space along each edge of the content in [Dp]. Note that the start and end
- * edges will be determined by the direction (clockwise or counterclockwise)
- *
- * @param outer Padding in the outward direction from the center of the
- * [CurvedLayout]
- * @param inner Padding in the inwards direction towards the center of the [CurvedLayout]
- * @param start Padding added at the start of the component.
- * @param end Padding added at the end of the component.
- */
-public fun ArcPaddingValues(
-    outer: Dp = 0.dp,
-    inner: Dp = 0.dp,
-    start: Dp = 0.dp,
-    end: Dp = 0.dp
-): ArcPaddingValues =
-    ArcPaddingValuesImpl(outer, inner, start, end)
-
-/**
- * Apply [all] dp of additional space along each edge of the content.
- */
-public fun ArcPaddingValues(all: Dp): ArcPaddingValues = ArcPaddingValuesImpl(all, all, all, all)
-
-/**
- * Apply [radial] dp of additional space on the edges towards and away from the center, and
- * [angular] dp before and after the component.
- */
-public fun ArcPaddingValues(radial: Dp = 0.dp, angular: Dp = 0.dp): ArcPaddingValues =
-    ArcPaddingValuesImpl(radial, radial, angular, angular)
-
-@Stable
-internal class ArcPaddingValuesImpl(val outer: Dp, val inner: Dp, val start: Dp, val end: Dp) :
-    ArcPaddingValues {
-    override fun equals(other: Any?): Boolean {
-        return other is ArcPaddingValuesImpl &&
-            outer == other.outer &&
-            inner == other.inner &&
-            start == other.start &&
-            end == other.end
-    }
-
-    override fun hashCode() = ((outer.hashCode() * 31 + inner.hashCode()) * 31 + start.hashCode()) *
-        31 + end.hashCode()
-
-    override fun toString(): String {
-        return "ArcPaddingValuesImpl(outer=$outer, inner=$inner, start=$start, end=$end)"
-    }
-
-    override fun calculateOuterPadding() = outer
-    override fun calculateInnerPadding() = inner
-    override fun calculateStartPadding() = start
-    override fun calculateEndPadding() = end
-}
 
 /**
  * [basicCurvedText] is a component allowing developers to easily write curved text following
@@ -123,8 +37,6 @@
  * those needs to be reversed in a Rtl layout.
  * If not specified, it will be inherited from the enclosing [curvedRow] or [CurvedLayout]
  * See [CurvedDirection.Angular].
- * @param contentArcPadding Allows to specify additional space along each "edge" of the content in
- * [Dp] see [ArcPaddingValues]
  * @param overflow How visual overflow should be handled.
  * @param style A @Composable factory to provide the style to use. This composable SHOULDN'T
  * generate any compose nodes.
@@ -133,13 +45,11 @@
     text: String,
     modifier: CurvedModifier = CurvedModifier,
     angularDirection: CurvedDirection.Angular? = null,
-    contentArcPadding: ArcPaddingValues = ArcPaddingValues(0.dp),
     overflow: TextOverflow = TextOverflow.Clip,
     style: @Composable () -> CurvedTextStyle = { CurvedTextStyle() }
 ) = add(CurvedTextChild(
     text,
     curvedLayoutDirection.copy(overrideAngular = angularDirection).clockwise(),
-    contentArcPadding,
     style,
     overflow
 ), modifier)
@@ -159,8 +69,6 @@
  * those needs to be reversed in a Rtl layout.
  * If not specified, it will be inherited from the enclosing [curvedRow] or [CurvedLayout]
  * See [CurvedDirection.Angular].
- * @param contentArcPadding Allows to specify additional space along each "edge" of the content in
- * [Dp] see [ArcPaddingValues]
  * @param overflow How visual overflow should be handled.
  */
 public fun CurvedScope.basicCurvedText(
@@ -168,33 +76,23 @@
     style: CurvedTextStyle,
     modifier: CurvedModifier = CurvedModifier,
     angularDirection: CurvedDirection.Angular? = null,
-    // TODO: reimplement as modifiers
-    contentArcPadding: ArcPaddingValues = ArcPaddingValues(0.dp),
     overflow: TextOverflow = TextOverflow.Clip,
-) = basicCurvedText(text, modifier, angularDirection, contentArcPadding, overflow) { style }
+) = basicCurvedText(text, modifier, angularDirection, overflow) { style }
 
 internal class CurvedTextChild(
     val text: String,
     val clockwise: Boolean = true,
-    val contentArcPadding: ArcPaddingValues = ArcPaddingValues(0.dp),
     val style: @Composable () -> CurvedTextStyle = { CurvedTextStyle() },
     val overflow: TextOverflow
 ) : CurvedChild() {
     private val delegate: CurvedTextDelegate = CurvedTextDelegate()
     private lateinit var actualStyle: CurvedTextStyle
 
-    override fun MeasureScope.initializeMeasure(
+    override fun CurvedMeasureScope.initializeMeasure(
         measurables: List<Measurable>,
         index: Int
     ): Int {
-        // TODO: move padding into a CurvedModifier
-        val arcPaddingPx = ArcPaddingPx(
-            contentArcPadding.calculateOuterPadding().toPx(),
-            contentArcPadding.calculateInnerPadding().toPx(),
-            contentArcPadding.calculateStartPadding().toPx(),
-            contentArcPadding.calculateEndPadding().toPx()
-        )
-        delegate.updateIfNeeded(text, clockwise, actualStyle.fontSize.toPx(), arcPaddingPx)
+        delegate.updateIfNeeded(text, clockwise, actualStyle.fontSize.toPx())
         return index // No measurables where mapped.
     }
 
@@ -242,13 +140,6 @@
     }
 }
 
-internal data class ArcPaddingPx(
-    val outer: Float,
-    val inner: Float,
-    val before: Float,
-    val after: Float
-)
-
 internal expect class CurvedTextDelegate() {
     var textWidth: Float
     var textHeight: Float
@@ -257,8 +148,7 @@
     fun updateIfNeeded(
         text: String,
         clockwise: Boolean,
-        fontSizePx: Float,
-        arcPaddingPx: ArcPaddingPx
+        fontSizePx: Float
     )
 
     fun DrawScope.doDraw(
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedComposable.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedComposable.kt
index 5cdb192..d6c4559 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedComposable.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedComposable.kt
@@ -21,7 +21,6 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.Constraints
 import kotlin.math.PI
@@ -62,7 +61,7 @@
         Box(content = content)
     }
 
-    override fun MeasureScope.initializeMeasure(
+    override fun CurvedMeasureScope.initializeMeasure(
         measurables: List<Measurable>,
         index: Int
     ): Int {
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedContainer.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedContainer.kt
index bff6b0a..a71600b 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedContainer.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedContainer.kt
@@ -20,7 +20,6 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.Placeable
 
 /**
@@ -35,10 +34,6 @@
     internal fun add(node: CurvedChild, modifier: CurvedModifier) {
         nodes.add(modifier.wrap(node))
     }
-    internal fun initialize(contentBuilder: CurvedScope.() -> Unit) {
-        nodes.clear()
-        apply(contentBuilder)
-    }
 }
 
 /**
@@ -47,9 +42,9 @@
 internal abstract class ContainerChild(
     curvedLayoutDirection: CurvedLayoutDirection,
     internal val reverseLayout: Boolean,
-    private val contentBuilder: CurvedScope.() -> Unit
+    contentBuilder: CurvedScope.() -> Unit
 ) : CurvedChild() {
-    private val curvedContainerScope = CurvedScope(curvedLayoutDirection)
+    private val curvedContainerScope = CurvedScope(curvedLayoutDirection).apply(contentBuilder)
     internal val children get() = curvedContainerScope.nodes
 
     internal val childrenInLayoutOrder get() = children.indices.map { ix ->
@@ -58,20 +53,22 @@
 
     @Composable
     override fun SubComposition() {
-        // Ensure we subscribe this composition to any state reads on contentBuilder,
-        // and we keep our internal tree in sync with compose's tree.
-        curvedContainerScope.initialize(contentBuilder)
         children.forEach {
             it.SubComposition()
         }
     }
 
-    override fun MeasureScope.initializeMeasure(
+    override fun CurvedMeasureScope.initializeMeasure(
         measurables: List<Measurable>,
         index: Int
     ) = children.fold(index) { currentIndex, node ->
-            with(node) {
-                initializeMeasure(measurables, currentIndex)
+            with(CurvedMeasureScope(
+                subDensity = this,
+                curvedContainerScope.curvedLayoutDirection
+            )) {
+                with(node) {
+                    initializeMeasure(measurables, currentIndex)
+                }
             }
         }
 
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedDirection.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedDirection.kt
index 8be4e91..716be35 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedDirection.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedDirection.kt
@@ -29,6 +29,23 @@
     @Immutable
     @kotlin.jvm.JvmInline
     public value class Angular internal constructor(internal val value: Int) {
+        /**
+         * Determine if the layout is going clockwise or counter-clockwise given the layout
+         * direction and this angular layout direction.
+         *
+         * @param layoutDirection The [LayoutDirection] to resolve this with.
+         */
+        internal fun resolveClockwise(layoutDirection: LayoutDirection): Boolean {
+            val isLtr = layoutDirection == LayoutDirection.Ltr
+            return when (this) {
+                Reversed -> !isLtr
+                Clockwise -> true
+                CounterClockwise -> false
+                // Normal
+                else -> isLtr
+            }
+        }
+
         companion object {
             /**
              * Go in Clockwise direction for Ltr layout and Counter Clockwise for Rtl.
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedLayout.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedLayout.kt
index 199a7da..bb0110c 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedLayout.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedLayout.kt
@@ -19,10 +19,8 @@
 import androidx.compose.foundation.layout.Row
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawWithContent
@@ -30,9 +28,9 @@
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.LayoutDirection
 import kotlin.math.PI
 import kotlin.math.cos
@@ -109,11 +107,11 @@
     // Note that all angles in the function are in radians, and the anchor parameter is in degrees
 
     val curvedLayoutDirection = initialCurvedLayoutDirection(angularDirection)
-    val curvedRowChild by remember(curvedLayoutDirection, radialAlignment, contentBuilder) {
-        derivedStateOf {
-            CurvedRowChild(curvedLayoutDirection, radialAlignment, contentBuilder)
-        }
-    }
+
+    // Create the curved tree and subscribe to be recomposed when any part changes, this may not be
+    // optimal but since we have only one measure block (the one here) for all the curved layout,
+    // we still need to do most of the work when content changes.
+    val curvedRowChild = CurvedRowChild(curvedLayoutDirection, radialAlignment, contentBuilder)
 
     Layout(
         modifier = modifier.drawWithContent {
@@ -135,9 +133,11 @@
 
         // Give the curved row scope the information needed to measure and map measurables
         // to children.
-        with(curvedRowChild) {
-            val mapped = initializeMeasure(measurables, 0)
-            require(mapped == measurables.size)
+        with(CurvedMeasureScope(subDensity = this, curvedLayoutDirection)) {
+            with(curvedRowChild) {
+                val mapped = initializeMeasure(measurables, 0)
+                require(mapped == measurables.size)
+            }
         }
 
         curvedRowChild.estimateThickness(radius)
@@ -166,24 +166,14 @@
 // TODO() For performance, to avoid class extra creation/destruction, we could implement this class
 //  as a bit field.
 internal class CurvedLayoutDirection(
-    private val radial: CurvedDirection.Radial,
-    private val angular: CurvedDirection.Angular,
-    private val layoutDirection: LayoutDirection
+    internal val radial: CurvedDirection.Radial,
+    internal val angular: CurvedDirection.Angular,
+    internal val layoutDirection: LayoutDirection
 ) {
-    fun clockwise(): Boolean {
-        return when (angular) {
-            CurvedDirection.Angular.Reversed -> !isLtr()
-            CurvedDirection.Angular.Clockwise -> true
-            CurvedDirection.Angular.CounterClockwise -> false
-            // CurvedDirection.Angular.Clockwise
-            else -> isLtr()
-        }
-    }
+    fun clockwise(): Boolean = angular.resolveClockwise(layoutDirection)
 
     fun outsideIn(): Boolean = radial == CurvedDirection.Radial.OutsideIn
 
-    fun isLtr(): Boolean = layoutDirection == LayoutDirection.Ltr
-
     fun copy(
         overrideRadial: CurvedDirection.Radial? = null,
         overrideAngular: CurvedDirection.Angular? = null
@@ -266,6 +256,12 @@
     val measureRadius: Float, // TODO: remove this from here or generalize
 )
 
+// Similar to IntrinsicMeasureScope
+internal class CurvedMeasureScope(
+    val subDensity: Density,
+    val curvedLayoutDirection: CurvedLayoutDirection
+) : Density by subDensity
+
 /**
  * Base class for children of a [CurvedLayout].
  *
@@ -299,6 +295,10 @@
     internal val sweepRadians: Float
         get() = partialLayoutInfo.sweepRadians
 
+    // Exposed as is needed in padding modifiers to translate distances to angles.
+    internal val measureRadius: Float
+        get() = partialLayoutInfo.measureRadius
+
     /**
      * Compose the content. This may generate some compose-ui nodes, but has to match
      * initializeMeasure's matching behavior (initializeMeasure should return the index parameter +
@@ -316,7 +316,7 @@
      * @return The new index in the measurables array, taking into account how many items we
      * mapped.
      */
-    open fun MeasureScope.initializeMeasure(
+    open fun CurvedMeasureScope.initializeMeasure(
         measurables: List<Measurable>,
         index: Int
     ): Int = index
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt
index a2a49b4..1a6ebb62 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedModifier.kt
@@ -21,7 +21,6 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.Placeable
 
 /**
@@ -84,7 +83,7 @@
     @Composable
     override fun SubComposition() { wrapped.SubComposition() }
 
-    override fun MeasureScope.initializeMeasure(
+    override fun CurvedMeasureScope.initializeMeasure(
         measurables: List<Measurable>,
         index: Int
     ): Int = with(wrapped) {
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedPadding.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedPadding.kt
new file mode 100644
index 0000000..43be9c2
--- /dev/null
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedPadding.kt
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.foundation
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+
+/**
+ * Apply additional space along the edges of the content.
+ *
+ * @param paddingValues The [ArcPaddingValues] to use. See that class and factory methods to see
+ * how paddings can be specified.
+ */
+public fun CurvedModifier.padding(paddingValues: ArcPaddingValues) =
+    this.then { child -> PaddingWrapper(child, paddingValues) }
+
+/**
+ * Apply additional space along the edges of the content. Dimmensions are in dp. For before and
+ * after they will be considered as if they are at the midpoint of the content (for conversion
+ * between dimension and angle).
+ *
+ * @param outer The space to add to the outer edge of the content (away from the center of the
+ * containing CurvedLayout)
+ * @param inner The space to add to the inner edge of the content (towards the center of the
+ * containing CurvedLayout)
+ * @param before The space added before the component, if it was draw clockwise. This is the edge of
+ * the component with the "smallest" angle.
+ * @param after The space added after the component, if it was draw clockwise. This is the edge of
+ * the component with the "biggest" angle.
+ */
+public fun CurvedModifier.padding(outer: Dp, inner: Dp, before: Dp, after: Dp) =
+    padding(ArcPaddingValuesImpl(outer, inner, before, after))
+
+/**
+ * Apply [angular] dp space before and after the component, and [radial] dp space to the outer and
+ * inner edges.
+ *
+ * @param radial The space added to the outer and inner edges of the content, in dp.
+ * @param angular The space added before and after the content, in dp.
+ */
+public fun CurvedModifier.padding(radial: Dp = 0.dp, angular: Dp = 0.dp) =
+    padding(radial, radial, angular, angular)
+
+/**
+ * Apply [all] dp space around the component.
+ *
+ * @param all The space added to all edges.
+ */
+public fun CurvedModifier.padding(all: Dp = 0.dp) = padding(all, all, all, all)
+
+/**
+ * Apply additional space along each edge of the content in [Dp].
+ * See the [ArcPaddingValues] factories for convenient ways to
+ * build [ArcPaddingValues].
+ */
+@Stable
+public interface ArcPaddingValues {
+    /**
+     * Padding in the outward direction from the center of the [CurvedLayout]
+     */
+    fun calculateOuterPadding(radialDirection: CurvedDirection.Radial): Dp
+
+    /**
+     * Padding in the inwards direction towards the center of the [CurvedLayout]
+     */
+    fun calculateInnerPadding(radialDirection: CurvedDirection.Radial): Dp
+
+    /**
+     * Padding added before the component, if it was draw clockwise. This is the edge of the
+     * component with the "smallest" angle.
+     */
+    fun calculateAfterPadding(
+        layoutDirection: LayoutDirection,
+        angularDirection: CurvedDirection.Angular
+    ): Dp
+
+    /**
+     * Padding added after the component, if it was draw clockwise. This is the edge of the
+     * component with the "biggest" angle.
+     */
+    fun calculateBeforePadding(
+        layoutDirection: LayoutDirection,
+        angularDirection: CurvedDirection.Angular
+    ): Dp
+}
+
+/**
+ * Apply additional space along each edge of the content in [Dp]. Note that that all dimensions are
+ * applied to a concrete edge, indepenend on layout direction and curved layout direction.
+ *
+ * @param outer Padding in the outward direction from the center of the
+ * [CurvedLayout]
+ * @param inner Padding in the inwards direction towards the center of the [CurvedLayout]
+ * @param before Padding added before the component, if it was draw clockwise.
+ * @param after Padding added after the component, if it was draw clockwise.
+ */
+public fun ArcPaddingValues(
+    outer: Dp = 0.dp,
+    inner: Dp = 0.dp,
+    before: Dp = 0.dp,
+    after: Dp = 0.dp
+): ArcPaddingValues =
+    ArcPaddingValuesImpl(outer, inner, before, after)
+
+/**
+ * Apply [all] dp of additional space along each edge of the content.
+ */
+public fun ArcPaddingValues(all: Dp): ArcPaddingValues = ArcPaddingValuesImpl(all, all, all, all)
+
+/**
+ * Apply [radial] dp of additional space on the edges towards and away from the center, and
+ * [angular] dp before and after the component.
+ */
+public fun ArcPaddingValues(radial: Dp = 0.dp, angular: Dp = 0.dp): ArcPaddingValues =
+    ArcPaddingValuesImpl(radial, radial, angular, angular)
+
+@Immutable
+internal class ArcPaddingValuesImpl(val outer: Dp, val inner: Dp, val before: Dp, val after: Dp) :
+    ArcPaddingValues {
+    override fun equals(other: Any?): Boolean {
+        return other is ArcPaddingValuesImpl &&
+            outer == other.outer &&
+            inner == other.inner &&
+            before == other.before &&
+            after == other.after
+    }
+
+    override fun hashCode() = ((outer.hashCode() * 31 + inner.hashCode()) * 31 +
+        before.hashCode()) * 31 + after.hashCode()
+
+    override fun toString(): String {
+        return "ArcPaddingValuesImpl(outer=$outer, inner=$inner, before=$before, after=$after)"
+    }
+
+    override fun calculateOuterPadding(radialDirection: CurvedDirection.Radial) = outer
+    override fun calculateInnerPadding(radialDirection: CurvedDirection.Radial) = inner
+    override fun calculateBeforePadding(
+        layoutDirection: LayoutDirection,
+        angularDirection: CurvedDirection.Angular
+    ) = before
+    override fun calculateAfterPadding(
+        layoutDirection: LayoutDirection,
+        angularDirection: CurvedDirection.Angular
+    ) = after
+}
+
+internal class PaddingWrapper(
+    child: CurvedChild,
+    val paddingValues: ArcPaddingValues
+) : BaseCurvedChildWrapper(child) {
+    private var outerPx = 0f
+    private var innerPx = 0f
+    private var beforePx = 0f
+    private var afterPx = 0f
+
+    override fun CurvedMeasureScope.initializeMeasure(
+        measurables: List<Measurable>,
+        index: Int
+    ): Int {
+        outerPx = paddingValues.calculateOuterPadding(curvedLayoutDirection.radial).toPx()
+        innerPx = paddingValues.calculateInnerPadding(curvedLayoutDirection.radial).toPx()
+        beforePx = paddingValues.calculateBeforePadding(
+            curvedLayoutDirection.layoutDirection,
+            curvedLayoutDirection.angular
+        ).toPx()
+        afterPx = paddingValues.calculateAfterPadding(
+            curvedLayoutDirection.layoutDirection,
+            curvedLayoutDirection.angular
+        ).toPx()
+        return with(wrapped) {
+            initializeMeasure(measurables, index)
+        }
+    }
+
+    override fun doEstimateThickness(maxRadius: Float) = wrapped.estimateThickness(maxRadius) +
+        outerPx + innerPx
+
+    override fun doRadialPosition(
+        parentOuterRadius: Float,
+        parentThickness: Float
+    ): PartialLayoutInfo {
+        val partialLayoutInfo = wrapped.radialPosition(
+            parentOuterRadius - outerPx,
+            parentThickness - outerPx - innerPx
+        )
+        val angularPadding = (beforePx + afterPx) / partialLayoutInfo.measureRadius
+        return PartialLayoutInfo(
+            partialLayoutInfo.sweepRadians + angularPadding,
+            partialLayoutInfo.outerRadius + outerPx,
+            partialLayoutInfo.thickness + innerPx + outerPx,
+            partialLayoutInfo.measureRadius + outerPx
+        )
+    }
+
+    override fun doAngularPosition(
+        parentStartAngleRadians: Float,
+        parentSweepRadians: Float,
+        centerOffset: Offset
+    ): Float {
+        val startAngularPadding = beforePx / measureRadius
+        val angularPadding = (beforePx + afterPx) / measureRadius
+        return wrapped.angularPosition(
+            parentStartAngleRadians + startAngularPadding,
+            parentSweepRadians - angularPadding,
+            centerOffset
+        ) - startAngularPadding
+    }
+}
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedSize.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedSize.kt
index 1630e35..79c9786 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedSize.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedSize.kt
@@ -18,7 +18,6 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 
@@ -92,7 +91,7 @@
     private var minThicknessPx = 0f
     private var maxThicknessPx = 0f
 
-    override fun MeasureScope.initializeMeasure(
+    override fun CurvedMeasureScope.initializeMeasure(
         measurables: List<Measurable>,
         index: Int
     ): Int {
diff --git a/wear/compose/compose-foundation/src/desktopMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.desktop.kt b/wear/compose/compose-foundation/src/desktopMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.desktop.kt
index 695e7f4..8c4c9c4 100644
--- a/wear/compose/compose-foundation/src/desktopMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.desktop.kt
+++ b/wear/compose/compose-foundation/src/desktopMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.desktop.kt
@@ -28,8 +28,7 @@
     actual fun updateIfNeeded(
         text: String,
         clockwise: Boolean,
-        fontSizePx: Float,
-        arcPaddingPx: ArcPaddingPx
+        fontSizePx: Float
     ) {
         // TODO(b/194653251): Implement
         throw java.lang.RuntimeException("Not implemented")
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index b60025f..01ec528 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -1,6 +1,10 @@
 // Signature format: 4.0
 package androidx.wear.compose.material {
 
+  @androidx.compose.runtime.Immutable public final class AutoCenteringParams {
+    ctor public AutoCenteringParams(optional int itemIndex, optional int itemOffset);
+  }
+
   @androidx.compose.runtime.Stable public interface ButtonColors {
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> backgroundColor(boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> contentColor(boolean enabled);
@@ -40,9 +44,9 @@
   }
 
   public final class CardKt {
-    method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> appName, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> time, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long appColor, optional long timeColor, optional long titleColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> appName, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> time, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long appColor, optional long timeColor, optional long titleColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long titleColor, optional long timeColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long titleColor, optional long timeColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
   }
 
   @androidx.compose.runtime.Stable public interface ChipColors {
@@ -138,7 +142,7 @@
   }
 
   public final class CurvedTextKt {
-    method public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional long background, optional long color, optional long fontSize, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow);
+    method public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional long background, optional long color, optional long fontSize, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
   }
 
   public final class DefaultTimeSourceKt {
@@ -328,7 +332,7 @@
   }
 
   public final class ScalingLazyColumnKt {
-    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional boolean autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional androidx.wear.compose.material.AutoCenteringParams? autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
     method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
@@ -515,10 +519,6 @@
     property public final androidx.wear.compose.material.SwipeToDismissValue currentValue;
     property public final boolean isAnimationRunning;
     property public final androidx.wear.compose.material.SwipeToDismissValue targetValue;
-    field public static final androidx.wear.compose.material.SwipeToDismissBoxState.Companion Companion;
-  }
-
-  public static final class SwipeToDismissBoxState.Companion {
   }
 
   public enum SwipeToDismissKeys {
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index b285cf4..0d98e8b 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -1,6 +1,10 @@
 // Signature format: 4.0
 package androidx.wear.compose.material {
 
+  @androidx.compose.runtime.Immutable public final class AutoCenteringParams {
+    ctor public AutoCenteringParams(optional int itemIndex, optional int itemOffset);
+  }
+
   @androidx.compose.runtime.Stable public interface ButtonColors {
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> backgroundColor(boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> contentColor(boolean enabled);
@@ -40,9 +44,9 @@
   }
 
   public final class CardKt {
-    method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> appName, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> time, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long appColor, optional long timeColor, optional long titleColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> appName, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> time, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long appColor, optional long timeColor, optional long titleColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long titleColor, optional long timeColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long titleColor, optional long timeColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
   }
 
   @androidx.compose.runtime.Stable public interface ChipColors {
@@ -138,7 +142,7 @@
   }
 
   public final class CurvedTextKt {
-    method public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional long background, optional long color, optional long fontSize, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow);
+    method public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional long background, optional long color, optional long fontSize, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
   }
 
   public final class DefaultTimeSourceKt {
@@ -354,7 +358,7 @@
   }
 
   public final class ScalingLazyColumnKt {
-    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional boolean autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional androidx.wear.compose.material.AutoCenteringParams? autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
     method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
@@ -551,10 +555,6 @@
     property public final androidx.wear.compose.material.SwipeToDismissValue currentValue;
     property public final boolean isAnimationRunning;
     property public final androidx.wear.compose.material.SwipeToDismissValue targetValue;
-    field public static final androidx.wear.compose.material.SwipeToDismissBoxState.Companion Companion;
-  }
-
-  public static final class SwipeToDismissBoxState.Companion {
   }
 
   public enum SwipeToDismissKeys {
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index b60025f..01ec528 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -1,6 +1,10 @@
 // Signature format: 4.0
 package androidx.wear.compose.material {
 
+  @androidx.compose.runtime.Immutable public final class AutoCenteringParams {
+    ctor public AutoCenteringParams(optional int itemIndex, optional int itemOffset);
+  }
+
   @androidx.compose.runtime.Stable public interface ButtonColors {
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> backgroundColor(boolean enabled);
     method @androidx.compose.runtime.Composable public androidx.compose.runtime.State<androidx.compose.ui.graphics.Color> contentColor(boolean enabled);
@@ -40,9 +44,9 @@
   }
 
   public final class CardKt {
-    method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> appName, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> time, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long appColor, optional long timeColor, optional long titleColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void AppCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> appName, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> time, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? appImage, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long appColor, optional long timeColor, optional long titleColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void Card(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.semantics.Role? role, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long titleColor, optional long timeColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void TitleCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? time, optional androidx.compose.ui.graphics.painter.Painter backgroundPainter, optional long contentColor, optional long titleColor, optional long timeColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
   }
 
   @androidx.compose.runtime.Stable public interface ChipColors {
@@ -138,7 +142,7 @@
   }
 
   public final class CurvedTextKt {
-    method public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional long background, optional long color, optional long fontSize, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional androidx.wear.compose.foundation.ArcPaddingValues contentArcPadding, optional int overflow);
+    method public static void curvedText(androidx.wear.compose.foundation.CurvedScope, String text, optional androidx.wear.compose.foundation.CurvedModifier modifier, optional long background, optional long color, optional long fontSize, optional androidx.wear.compose.foundation.CurvedTextStyle? style, optional androidx.wear.compose.foundation.CurvedDirection.Angular? angularDirection, optional int overflow);
   }
 
   public final class DefaultTimeSourceKt {
@@ -328,7 +332,7 @@
   }
 
   public final class ScalingLazyColumnKt {
-    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional boolean autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingLazyListState state, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional boolean reverseLayout, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.gestures.FlingBehavior flingBehavior, optional boolean userScrollEnabled, optional androidx.wear.compose.material.ScalingParams scalingParams, optional int anchorType, optional androidx.wear.compose.material.AutoCenteringParams? autoCentering, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyListScope,kotlin.Unit> content);
     method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void items(androidx.wear.compose.material.ScalingLazyListScope, T![] items, optional kotlin.jvm.functions.Function1<? super T,?>? key, kotlin.jvm.functions.Function2<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super T,kotlin.Unit> itemContent);
     method public static inline <T> void itemsIndexed(androidx.wear.compose.material.ScalingLazyListScope, java.util.List<? extends T> items, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super T,?>? key, kotlin.jvm.functions.Function3<? super androidx.wear.compose.material.ScalingLazyListItemScope,? super java.lang.Integer,? super T,kotlin.Unit> itemContent);
@@ -515,10 +519,6 @@
     property public final androidx.wear.compose.material.SwipeToDismissValue currentValue;
     property public final boolean isAnimationRunning;
     property public final androidx.wear.compose.material.SwipeToDismissValue targetValue;
-    field public static final androidx.wear.compose.material.SwipeToDismissBoxState.Companion Companion;
-  }
-
-  public static final class SwipeToDismissBoxState.Companion {
   }
 
   public enum SwipeToDismissKeys {
diff --git a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScalingLazyColumnSample.kt b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScalingLazyColumnSample.kt
index f38a020..976c4ce2 100644
--- a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScalingLazyColumnSample.kt
+++ b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScalingLazyColumnSample.kt
@@ -23,6 +23,7 @@
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.AutoCenteringParams
 import androidx.wear.compose.material.Chip
 import androidx.wear.compose.material.ChipDefaults
 import androidx.wear.compose.material.ListHeader
@@ -92,7 +93,8 @@
     ScalingLazyColumn(
         anchorType = ScalingLazyListAnchorType.ItemStart,
         verticalArrangement = Arrangement.spacedBy(itemSpacing),
-        state = state
+        state = state,
+        autoCentering = AutoCenteringParams(itemOffset = scrollOffset)
     ) {
         item {
             ListHeader {
@@ -119,7 +121,7 @@
 fun SimpleScalingLazyColumnWithContentPadding() {
     ScalingLazyColumn(
         contentPadding = PaddingValues(top = 20.dp, bottom = 20.dp),
-        autoCentering = false
+        autoCentering = null
     ) {
         item {
             ListHeader {
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/CardTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/CardTest.kt
index f516fda..63805c3 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/CardTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/CardTest.kt
@@ -184,6 +184,110 @@
     }
 }
 
+public class AppCardTest {
+    @get:Rule
+    public val rule: ComposeContentTestRule = createComposeRule()
+
+    @Test
+    public fun responds_to_click_when_enabled() {
+        var clicked = false
+
+        rule.setContentWithTheme {
+            AppCard(
+                onClick = { clicked = true },
+                enabled = true,
+                appName = {},
+                time = {},
+                title = {},
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                TestImage()
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performClick()
+
+        rule.runOnIdle {
+            assertEquals(true, clicked)
+        }
+    }
+
+    @Test
+    public fun does_not_respond_to_click_when_disabled() {
+        var clicked = false
+
+        rule.setContentWithTheme {
+            AppCard(
+                onClick = { clicked = true },
+                appName = {},
+                time = {},
+                title = {},
+                enabled = false,
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                TestImage()
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performClick()
+
+        rule.runOnIdle {
+            assertEquals(false, clicked)
+        }
+    }
+}
+
+public class TitleCardTest {
+    @get:Rule
+    public val rule: ComposeContentTestRule = createComposeRule()
+
+    @Test
+    public fun responds_to_click_when_enabled() {
+        var clicked = false
+
+        rule.setContentWithTheme {
+            TitleCard(
+                onClick = { clicked = true },
+                enabled = true,
+                time = {},
+                title = {},
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                TestImage()
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performClick()
+
+        rule.runOnIdle {
+            assertEquals(true, clicked)
+        }
+    }
+
+    @Test
+    public fun does_not_respond_to_click_when_disabled() {
+        var clicked = false
+
+        rule.setContentWithTheme {
+            TitleCard(
+                onClick = { clicked = true },
+                enabled = false,
+                time = {},
+                title = {},
+                modifier = Modifier.testTag(TEST_TAG)
+            ) {
+                TestImage()
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG).performClick()
+
+        rule.runOnIdle {
+            assertEquals(false, clicked)
+        }
+    }
+}
+
 public class CardSizeTest {
     @get:Rule
     public val rule: ComposeContentTestRule = createComposeRule()
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
index dd3eb02..4ea531b 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
@@ -118,7 +118,7 @@
                 modifier = Modifier
                     .onSizeChanged { viewPortHeight = it.height }
                     .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f),
-                autoCentering = false
+                autoCentering = null
             ) {
                 items(3) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -189,7 +189,7 @@
     @Test
     fun scrollableScalingLazyColumnGivesCorrectPositionAndSize() {
         scrollableScalingLazyColumnPositionAndSize(
-            autoCentering = true,
+            enableAutoCentering = true,
             contentPadding = PaddingValues(0.dp)
         )
     }
@@ -197,7 +197,7 @@
     @Test
     fun scrollableScalingLazyColumnGivesCorrectPositionAndSizeWithContentPadding() {
         scrollableScalingLazyColumnPositionAndSize(
-            autoCentering = true,
+            enableAutoCentering = true,
             contentPadding = PaddingValues(50.dp)
         )
     }
@@ -205,13 +205,13 @@
     @Test
     fun scrollableScalingLazyColumnGivesCorrectPositionAndSizeWithContentPaddingNoAutoCenter() {
         scrollableScalingLazyColumnPositionAndSize(
-            autoCentering = false,
+            enableAutoCentering = false,
             contentPadding = PaddingValues(50.dp)
         )
     }
 
     private fun scrollableScalingLazyColumnPositionAndSize(
-        autoCentering: Boolean,
+        enableAutoCentering: Boolean,
         contentPadding: PaddingValues
     ) {
         lateinit var state: ScalingLazyListState
@@ -230,7 +230,8 @@
                         itemSizeDp * 3f + itemSpacingDp * 2f
                     ),
                 scalingParams = ScalingLazyColumnDefaults.scalingParams(edgeScale = 1.0f),
-                autoCentering = autoCentering,
+                autoCentering = if (enableAutoCentering)
+                    AutoCenteringParams(itemIndex = 0) else null,
                 contentPadding = contentPadding
             ) {
                 items(5) {
@@ -325,7 +326,7 @@
                     .fillMaxWidth()
                     .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
                     .background(Color.DarkGray),
-                autoCentering = false
+                autoCentering = null
             ) {
                 items(3) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -371,7 +372,7 @@
                     )
                     .background(Color.DarkGray),
                 scalingParams = ScalingLazyColumnDefaults.scalingParams(edgeScale = 1.0f),
-                autoCentering = false
+                autoCentering = null
             ) {
                 items(5) {
                     Box(Modifier.requiredSize(itemSizeDp))
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnIndexedTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnIndexedTest.kt
index 8d050db..1a3cbe7 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnIndexedTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnIndexedTest.kt
@@ -98,7 +98,7 @@
                 state = rememberScalingLazyListState(initialCenterItemIndex = 0)
                     .also { state = it },
                 modifier = Modifier.height(200.dp),
-                autoCentering = false,
+                autoCentering = null,
                 scalingParams = ScalingLazyColumnDefaults.scalingParams(edgeScale = 1.0f)
             ) {
                 itemsIndexed(items) { index, item ->
@@ -130,7 +130,7 @@
                 state = rememberScalingLazyListState(initialCenterItemIndex = 0)
                     .also { state = it },
                 modifier = Modifier.height(viewPortHeight),
-                autoCentering = true,
+                autoCentering = AutoCenteringParams(itemIndex = 0),
                 verticalArrangement = Arrangement.spacedBy(gapBetweenItems),
                 // No scaling as we are doing maths with expected item sizes
                 scalingParams = ScalingLazyColumnDefaults.scalingParams(edgeScale = 1.0f)
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
index 3d17741..850300e 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyColumnTest.kt
@@ -87,7 +87,7 @@
                     state = rememberScalingLazyListState().also { state = it },
                     modifier = Modifier.testTag(TEST_TAG).requiredSize(listSize),
                     anchorType = ScalingLazyListAnchorType.ItemCenter,
-                    autoCentering = true,
+                    autoCentering = AutoCenteringParams(),
                     verticalArrangement = Arrangement.spacedBy(0.dp),
                     scalingParams = ScalingLazyColumnDefaults.scalingParams(
                         edgeScale = 0f,
@@ -139,7 +139,7 @@
                     state = rememberScalingLazyListState().also { state = it },
                     modifier = Modifier.testTag(TEST_TAG).requiredSize(listSize),
                     anchorType = ScalingLazyListAnchorType.ItemStart,
-                    autoCentering = true,
+                    autoCentering = AutoCenteringParams(),
                     verticalArrangement = Arrangement.spacedBy(0.dp),
                     scalingParams = ScalingLazyColumnDefaults.scalingParams(
                         edgeScale = 0f,
@@ -192,7 +192,7 @@
                     modifier = Modifier.testTag(TEST_TAG).requiredSize(
                         itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                     ),
-                    autoCentering = false
+                    autoCentering = null
                 ) {
                     items(5) {
                         Box(Modifier.requiredSize(itemSizeDp))
@@ -224,7 +224,7 @@
                     modifier = Modifier.testTag(TEST_TAG).requiredSize(
                         itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                     ),
-                    autoCentering = false,
+                    autoCentering = null,
                     userScrollEnabled = false
                 ) {
                     items(5) {
@@ -259,7 +259,7 @@
                     modifier = Modifier.testTag(TEST_TAG).requiredSize(
                         itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                     ),
-                    autoCentering = true
+                    autoCentering = AutoCenteringParams(itemIndex = 0)
                 ) {
                     items(5) {
                         Box(Modifier.requiredSize(itemSizeDp))
@@ -292,7 +292,7 @@
                     modifier = Modifier.testTag(TEST_TAG).requiredSize(
                         itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                     ),
-                    autoCentering = true,
+                    autoCentering = AutoCenteringParams(itemIndex = 0),
                     flingBehavior = ScalingLazyColumnDefaults.snapFlingBehavior(state)
                 ) {
                     items(5) {
@@ -337,7 +337,7 @@
                     modifier = Modifier.testTag(TEST_TAG).requiredSize(
                         itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                     ),
-                    autoCentering = true,
+                    autoCentering = AutoCenteringParams(itemIndex = 0),
                     flingBehavior = ScalingLazyColumnDefaults.snapFlingBehavior(
                         state = state,
                         snapOffset = snapOffset
@@ -381,7 +381,7 @@
                         itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                     ),
                     reverseLayout = true,
-                    autoCentering = false
+                    autoCentering = null
                 ) {
                     items(5) {
                         Box(Modifier.requiredSize(itemSizeDp))
@@ -414,6 +414,7 @@
                         itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                     ),
                     scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                    autoCentering = AutoCenteringParams(itemIndex = 0)
                 ) {
                     items(5) {
                         Box(Modifier.requiredSize(itemSizeDp).testTag("Item:" + it))
@@ -492,7 +493,7 @@
                     modifier = Modifier
                         .testTag(TEST_TAG)
                         .requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
-                    autoCentering = false
+                    autoCentering = null
                 ) {
                     items(6) {
                         Box(Modifier.requiredSize(itemSizeDp))
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
index a1ae7e9..c39f251 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
@@ -77,7 +77,7 @@
                 modifier = Modifier.requiredSize(
                     itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                 ),
-                autoCentering = true
+                autoCentering = AutoCenteringParams()
             ) {
                 items(5) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -103,7 +103,7 @@
                 modifier = Modifier.requiredSize(
                     itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                 ),
-                autoCentering = true,
+                autoCentering = AutoCenteringParams(),
                 contentPadding = PaddingValues(all = 0.dp)
             ) {
                 items(5) {
@@ -129,7 +129,7 @@
                 modifier = Modifier.requiredSize(
                     itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                 ),
-                autoCentering = true,
+                autoCentering = AutoCenteringParams(),
                 contentPadding = PaddingValues(all = 0.dp)
             ) {
                 items(5) {
@@ -155,7 +155,7 @@
                 modifier = Modifier.requiredSize(
                     itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                 ),
-                autoCentering = true,
+                autoCentering = AutoCenteringParams(),
                 contentPadding = PaddingValues(all = 0.dp),
                 reverseLayout = true
             ) {
@@ -183,7 +183,7 @@
                 modifier = Modifier.requiredSize(
                     itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                 ),
-                autoCentering = true
+                autoCentering = AutoCenteringParams(itemIndex = 0)
             ) {
                 items(5) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -209,7 +209,7 @@
                 modifier = Modifier.requiredSize(
                     itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                 ),
-                autoCentering = false
+                autoCentering = null
             ) {
                 items(5) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -234,7 +234,7 @@
                     itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                 ),
                 reverseLayout = true,
-                autoCentering = false
+                autoCentering = null
             ) {
                 items(5) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -261,7 +261,7 @@
                     itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                 ),
                 reverseLayout = true,
-                autoCentering = true
+                autoCentering = AutoCenteringParams(itemIndex = 0)
             ) {
                 items(5) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -287,7 +287,7 @@
                 modifier = Modifier.requiredSize(
                     itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                 ),
-                autoCentering = false
+                autoCentering = null
             ) {
                 items(5) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -314,7 +314,7 @@
                 modifier = Modifier.requiredSize(
                     itemSizeDp
                 ),
-                autoCentering = false
+                autoCentering = null
             ) {
                 items(5) {
                     Box(Modifier.requiredSize(itemSizeDp * 5))
@@ -404,7 +404,7 @@
                     itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                 ),
                 reverseLayout = true,
-                autoCentering = false
+                autoCentering = null
             ) {
                 items(5) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -569,6 +569,7 @@
                     itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                 ),
                 scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                autoCentering = AutoCenteringParams(itemIndex = 0)
             ) {
                 items(5) {
                     Box(
@@ -661,6 +662,7 @@
                     itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f
                 ),
                 scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
+                autoCentering = AutoCenteringParams(itemIndex = 0)
             ) {
                 items(5) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -700,7 +702,7 @@
                 ),
                 scalingParams = ScalingLazyColumnDefaults.scalingParams(1.0f, 1.0f),
                 reverseLayout = true,
-                autoCentering = false
+                autoCentering = null
             ) {
                 items(5) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -736,7 +738,7 @@
                 state = rememberScalingLazyListState().also { state = it },
                 modifier = Modifier.requiredSize(itemSizeDp * 3.5f + spacing * 2.5f),
                 verticalArrangement = Arrangement.spacedBy(spacing),
-                autoCentering = false
+                autoCentering = null
             ) {
                 items(5) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -773,7 +775,7 @@
             ScalingLazyColumn(
                 state = rememberScalingLazyListState().also { state = it },
                 modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
-                autoCentering = false
+                autoCentering = null
             ) {
                 items(6) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -806,7 +808,7 @@
             ScalingLazyColumn(
                 state = rememberScalingLazyListState().also { state = it },
                 modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
-                autoCentering = false
+                autoCentering = null
             ) {
                 items(6) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -892,7 +894,7 @@
             ScalingLazyColumn(
                 state = rememberScalingLazyListState().also { state = it },
                 modifier = Modifier.requiredSize(itemSizeDp * 3.5f + defaultItemSpacingDp * 2.5f),
-                autoCentering = false
+                autoCentering = null
             ) {
                 items(6) {
                     Box(Modifier.requiredSize(itemSizeDp))
@@ -1026,7 +1028,7 @@
                 state = rememberScalingLazyListState(
                     initialCenterItemIndex = 0
                 ).also { state = it },
-                autoCentering = true
+                autoCentering = AutoCenteringParams()
             ) {
                 items(7) {
                     Box(Modifier.requiredSize(itemSizeDp))
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
index ca6aaa8..90eadf9 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
@@ -16,21 +16,21 @@
 
 package androidx.wear.compose.material
 
+import androidx.compose.foundation.Image
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.IntrinsicSize
-import androidx.compose.foundation.layout.RowScope
 import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
@@ -174,6 +174,8 @@
  * @param title A slot for displaying the title of the card, expected to be one or two lines of
  * start aligned text of [Typography.button]
  * @param modifier Modifier to be applied to the card
+ * @param enabled Controls the enabled state of the card. When `false`, this card will not
+ * be clickable
  * @param appImage A slot for a small ([CardDefaults.AppImageSize]x[CardDefaults.AppImageSize] )
  * [Image] associated with the application.
  * @param backgroundPainter A painter used to paint the background of the card. A card will
@@ -192,6 +194,7 @@
     time: @Composable RowScope.() -> Unit,
     title: @Composable RowScope.() -> Unit,
     modifier: Modifier = Modifier,
+    enabled: Boolean = true,
     appImage: @Composable (RowScope.() -> Unit)? = null,
     backgroundPainter: Painter = CardDefaults.cardBackgroundPainter(),
     contentColor: Color = MaterialTheme.colors.onSurfaceVariant,
@@ -204,7 +207,7 @@
         onClick = onClick,
         modifier = modifier,
         backgroundPainter = backgroundPainter,
-        enabled = true,
+        enabled = enabled,
     ) {
         Column {
             Row(
@@ -283,6 +286,8 @@
  * @param title A slot for displaying the title of the card, expected to be one or two lines of text
  * of [Typography.button]
  * @param modifier Modifier to be applied to the card
+ * @param enabled Controls the enabled state of the card. When `false`, this card will not
+ * be clickable
  * @param time An optional slot for displaying the time relevant to the contents of the card,
  * expected to be a short piece of end aligned text.
  * @param backgroundPainter A painter used to paint the background of the card. A title card can
@@ -298,6 +303,7 @@
     onClick: () -> Unit,
     title: @Composable RowScope.() -> Unit,
     modifier: Modifier = Modifier,
+    enabled: Boolean = true,
     time: @Composable (RowScope.() -> Unit)? = null,
     backgroundPainter: Painter = CardDefaults.cardBackgroundPainter(),
     contentColor: Color = MaterialTheme.colors.onSurfaceVariant,
@@ -309,7 +315,7 @@
         onClick = onClick,
         modifier = modifier,
         backgroundPainter = backgroundPainter,
-        enabled = true,
+        enabled = enabled,
     ) {
         Column {
             Row(
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/CurvedText.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/CurvedText.kt
index eb8da4f..efc3cda 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/CurvedText.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/CurvedText.kt
@@ -20,16 +20,13 @@
 import androidx.compose.ui.graphics.takeOrElse
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.TextUnit
-import androidx.compose.ui.unit.dp
-import androidx.wear.compose.foundation.ArcPaddingValues
 import androidx.wear.compose.foundation.CurvedDirection
 import androidx.wear.compose.foundation.CurvedLayout
-import androidx.wear.compose.foundation.basicCurvedText
 import androidx.wear.compose.foundation.CurvedModifier
 import androidx.wear.compose.foundation.CurvedScope
 import androidx.wear.compose.foundation.CurvedTextStyle
+import androidx.wear.compose.foundation.basicCurvedText
 import androidx.wear.compose.foundation.curvedRow
 
 /**
@@ -74,8 +71,6 @@
  * those needs to be reversed in a Rtl layout.
  * If not specified, it will be inherited from the enclosing [curvedRow] or [CurvedLayout]
  * See [CurvedDirection.Angular].
- * @param contentArcPadding Allows to specify additional space along each "edge" of the content in
- * [Dp] see [ArcPaddingValues]
  * @param overflow How visual overflow should be handled.
  */
 public fun CurvedScope.curvedText(
@@ -86,9 +81,8 @@
     fontSize: TextUnit = TextUnit.Unspecified,
     style: CurvedTextStyle? = null,
     angularDirection: CurvedDirection.Angular? = null,
-    contentArcPadding: ArcPaddingValues = ArcPaddingValues(0.dp),
     overflow: TextOverflow = TextOverflow.Clip,
-) = basicCurvedText(text, modifier, angularDirection, contentArcPadding, overflow) {
+) = basicCurvedText(text, modifier, angularDirection, overflow) {
     val baseStyle = style ?: CurvedTextStyle(LocalTextStyle.current)
     val textColor = color.takeOrElse {
         baseStyle.color.takeOrElse {
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
index 9b737ad..2bcb66a 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
@@ -200,6 +200,69 @@
     }
 }
 
+/**
+ * Parameters to determine which list item and offset to calculate auto-centering spacing for. The
+ * default values are [itemIndex] = 1 and [itemOffset] = 0. This will provide sufficient padding for
+ * the second item (index = 1) in the list being centerable. This is to match the Wear UX
+ * guidelines that a typical list will have a ListHeader item as the first item in the list
+ * (index = 0) and that this should not be scrollable into the middle of the viewport, instead the
+ * first list item that a user can interact with (index = 1) would be the first that would be in the
+ * center.
+ *
+ * If your use case is different and you want all list items to be able to be scrolled to the
+ * viewport middle, including the first item in the list then set [itemIndex] = 0.
+ *
+ * The higher the value for [itemIndex] you provide the less auto centering padding will be
+ * provided as the amount of padding needed to allow that item to be centered will reduce.
+ * Even for a list of short items (such as [CompactChip]) setting [itemIndex] above 3 or 4 is likely
+ * to result in no auto-centering padding being provided as items with index 3 or 4 will probably
+ * already be naturally scrollable to the center of the viewport.
+ *
+ * [itemOffset] allows adjustment of the items position relative the [ScalingLazyColumn]s
+ * [ScalingLazyListAnchorType]. This can be useful if you need fine grained control over item
+ * positioning and spacing, e.g. If you are lining up the gaps between two items on the viewport
+ * center line where you would want to set the offset to half the distance between listItems in
+ * pixels.
+ *
+ * See also [rememberScalingLazyListState] where similar fields are provided to allow control over
+ * the initially selected centered item index and offset. By default these match the auto centering
+ * defaults meaning that the second item (index = 1) will be the item scrolled to the viewport
+ * center.
+ *
+ * @param itemIndex Which list item index to enable auto-centering from. Space (padding) will be
+ * added such that items with index [itemIndex] or greater will be able to be scrolled to the center
+ * of the viewport. If the developer wants to add additional space to allow other list items to also
+ * be scrollable to the center they can use contentPadding on the ScalingLazyColumn. If the
+ * developer wants custom control over position and spacing they can switch off autoCentering
+ * and provide contentPadding.
+ *
+ * @param itemOffset What offset, if any, to apply when calculating space for auto-centering
+ * the [itemIndex] item. E.g. itemOffset can be used if the developer wants to align the viewport
+ * center in the gap between two list items.
+ *
+ * For an example of a [ScalingLazyColumn] with an explicit itemOffset see:
+ * @sample androidx.wear.compose.material.samples.ScalingLazyColumnEdgeAnchoredAndAnimatedScrollTo
+ */
+@Immutable
+public class AutoCenteringParams(
+    // @IntRange(from = 0)
+    internal val itemIndex: Int = 1,
+    internal val itemOffset: Int = 0,
+) {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        return (other is AutoCenteringParams) &&
+            itemIndex == other.itemIndex &&
+            itemOffset == other.itemOffset
+    }
+
+    override fun hashCode(): Int {
+        var result = itemIndex
+        result = 31 * result + itemOffset
+        return result
+    }
+}
+
 internal fun convertToCenterOffset(
     anchorType: ScalingLazyListAnchorType,
     itemScrollOffset: Int,
@@ -262,14 +325,14 @@
  * @param scalingParams The parameters to configure the scaling and transparency effects for the
  * component
  * @param anchorType How to anchor list items to the center-line of the viewport
- * @param autoCentering Flag to indicate whether space/padding should be automatically added to make
- * sure that list items can be scrolled into the center of the viewport (based on their
- * [anchorType]). If true then space will be added before the first list item, if needed, to ensure
- * that items with indexes greater than or equal to the initialCenterItemIndex passed to
- * [ScalingLazyListState] will be able to be scrolled to the center. Similarly space will be added
- * at the end of the list to ensure that items can be scrolled up to the center. If false no
- * automatic space will be added and instead the developer can use [contentPadding] to manually
- * arrange the items.
+ * @param autoCentering AutoCenteringParams parameter to control whether space/padding should be
+ * automatically added to make sure that list items can be scrolled into the center of the viewport
+ * (based on their [anchorType]). If non-null then space will be added before the first list item,
+ * if needed, to ensure that items with indexes greater than or equal to the itemIndex (offset by
+ * itemOffset pixels) will be able to be scrolled to the center of the viewport. Similarly space
+ * will be added at the end of the list to ensure that items can be scrolled up to the center. If
+ * null no automatic space will be added and instead the developer can use [contentPadding] to
+ * manually arrange the items.
  */
 @Composable
 public fun ScalingLazyColumn(
@@ -287,7 +350,7 @@
     userScrollEnabled: Boolean = true,
     scalingParams: ScalingParams = ScalingLazyColumnDefaults.scalingParams(),
     anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter,
-    autoCentering: Boolean = true,
+    autoCentering: AutoCenteringParams? = AutoCenteringParams(),
     content: ScalingLazyListScope.() -> Unit
 ) {
     var initialized by remember { mutableStateOf(false) }
@@ -360,7 +423,7 @@
                 )
                 // Only add spacers if autoCentering == true as we have to consider the impact of
                 // vertical spacing between items.
-                if (autoCentering) {
+                if (autoCentering != null) {
                     item {
                         Spacer(
                             modifier = Modifier.height(state.topAutoCenteringItemSizePx.toDp())
@@ -368,7 +431,7 @@
                     }
                 }
                 scope.content()
-                if (autoCentering) {
+                if (autoCentering != null) {
                     item {
                         Spacer(
                             modifier = Modifier.height(state.bottomAutoCenteringItemSizePx.toDp())
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
index f5a3607..88e9896 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
@@ -346,7 +346,7 @@
     scalingParams: ScalingParams,
     beforeContentPaddingPx: Int,
     anchorType: ScalingLazyListAnchorType,
-    autoCentering: Boolean,
+    autoCentering: AutoCenteringParams?,
     initialized: Boolean
 ): ScalingLazyListItemInfo {
     val adjustedItemStart = itemStart - verticalAdjustment
@@ -381,7 +381,7 @@
     )
     return DefaultScalingLazyListItemInfo(
         // Adjust index to take into account the Spacer before the first list item
-        index = if (autoCentering) item.index - 1 else item.index,
+        index = if (autoCentering != null) item.index - 1 else item.index,
         key = item.key,
         unadjustedOffset = unadjustedOffset,
         offset = offset,
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
index 04e33f8..6ba18f5 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
@@ -39,12 +39,7 @@
  *
  * @param initialCenterItemIndex the initial value for [ScalingLazyListState.centerItemIndex],
  * defaults to 1. This will place the 2nd list item (index == 1) in the center of the viewport and
- * the first item (index == 0) before it. Note that if [ScalingLazyColumn] autoCentering property is
- * set to true then space (padding) will be added such that items with index
- * [initialCenterItemIndex] or greater will be able to be scrolled to the center of the viewport. If
- * the developer wants to add additional space to allow other list items to also be scrollable to
- * the center they can use contentPadding on the ScalingLazyColumn. If the developer wants custom
- * control over position and spacing they can switch off autoCentering and provide contentPadding.
+ * the first item (index == 0) before it.
  *
  * @param initialCenterItemScrollOffset the initial value for
  * [ScalingLazyListState.centerItemScrollOffset] in pixels
@@ -69,11 +64,7 @@
  *
  * @param initialCenterItemIndex the initial value for [ScalingLazyListState.centerItemIndex],
  * defaults to 1. This will place the 2nd list item (index == 1) in the center of the viewport and
- * the first item (index == 0) before it. Note that if [ScalingLazyColumn] autoCentering property is
- * set to true then space (padding) will be added such that items with index
- * [initialCenterItemIndex] or greater will be able to be scrolled to the center of the viewport. If
- * the developer wants to add additional space to allow other list items to also be scrollable to
- * the center they can use contentPadding on the ScalingLazyColumn.
+ * the first item (index == 0) before it.
  *
  * If the developer wants custom control over position and spacing they can switch off autoCentering
  * and provide contentPadding.
@@ -96,7 +87,7 @@
     internal val viewportHeightPx = mutableStateOf<Int?>(null)
     internal val reverseLayout = mutableStateOf<Boolean?>(null)
     internal val anchorType = mutableStateOf<ScalingLazyListAnchorType?>(null)
-    internal val autoCentering = mutableStateOf<Boolean?>(null)
+    internal val autoCentering = mutableStateOf<AutoCenteringParams?>(null)
     internal val initialized = mutableStateOf<Boolean>(false)
 
     /**
@@ -112,7 +103,7 @@
             gapBetweenItemsPx.value == null || viewportHeightPx.value == null ||
             anchorType.value == null || reverseLayout.value == null ||
             beforeContentPaddingPx.value == null || autoCentering.value == null ||
-            !autoCentering.value!! || layoutInfo.visibleItemsInfo.isEmpty()
+            autoCentering.value == null || layoutInfo.visibleItemsInfo.isEmpty()
         ) {
             0
         } else {
@@ -150,7 +141,7 @@
         if (extraPaddingPx.value == null || scalingParams.value == null ||
             gapBetweenItemsPx.value == null || viewportHeightPx.value == null ||
             anchorType.value == null || reverseLayout.value == null ||
-            beforeContentPaddingPx.value == null || autoCentering.value == null
+            beforeContentPaddingPx.value == null
         ) {
             EmptyScalingLazyListLayoutInfo
         } else {
@@ -184,7 +175,7 @@
                     scalingParams.value!!,
                     beforeContentPaddingPx.value!!,
                     anchorType.value!!,
-                    autoCentering.value!!,
+                    autoCentering.value,
                     initialized.value
                 )
                 visibleItemsInfo.add(
@@ -227,7 +218,7 @@
                                 scalingParams.value!!,
                                 beforeContentPaddingPx.value!!,
                                 anchorType.value!!,
-                                autoCentering.value!!,
+                                autoCentering.value,
                                 initialized.value
                             )
                             visibleItemsInfo.add(0, itemInfo)
@@ -261,7 +252,7 @@
                                 scalingParams.value!!,
                                 beforeContentPaddingPx.value!!,
                                 anchorType.value!!,
-                                autoCentering.value!!,
+                                autoCentering.value,
                                 initialized.value
                             )
 
@@ -274,7 +265,7 @@
                 }
             }
             val totalItemsCount =
-                if (autoCentering.value!!) {
+                if (autoCentering.value != null) {
                     (lazyListState.layoutInfo.totalItemsCount - 2).coerceAtLeast(0)
                 } else {
                     lazyListState.layoutInfo.totalItemsCount
@@ -290,7 +281,7 @@
                 // Not already initialized
                 !initialized.value && (
                     // Not autoCentering
-                    !autoCentering.value!! || (
+                    autoCentering.value == null || (
                         lazyListState.layoutInfo.visibleItemsInfo.size >= 2 && (
                             // or Empty list (other than the 2 spacers)
                             lazyListState.layoutInfo.visibleItemsInfo.size == 2 ||
@@ -411,7 +402,7 @@
             initialCenterItemScrollOffset = scrollOffset
             return
         }
-        val lazyListStateIndex = if (autoCentering.value!!) index + 1 else index
+        val lazyListStateIndex = if (autoCentering.value != null) index + 1 else index
         val offsetToCenterOfViewport =
             beforeContentPaddingPx.value!! - (viewportHeightPx.value!! / 2)
         if (anchorType.value == ScalingLazyListAnchorType.ItemStart) {
@@ -462,7 +453,7 @@
     ) {
         // Convert the index to take into account the Spacer added to the underlying LazyList before
         // the first ScalingLazyColumn list item
-        val lazyListStateIndex = if (autoCentering.value!!) index + 1 else index
+        val lazyListStateIndex = if (autoCentering.value != null) index + 1 else index
         val offsetToCenterOfViewport =
             beforeContentPaddingPx.value!! - (viewportHeightPx.value!! / 2)
         if (anchorType.value == ScalingLazyListAnchorType.ItemStart) {
@@ -492,24 +483,24 @@
     }
 
     private fun discardAutoCenteringListItem(item: LazyListItemInfo): Boolean =
-        autoCentering.value!! &&
+        autoCentering.value != null &&
             (item.index == 0 || item.index == lazyListState.layoutInfo.totalItemsCount - 1)
 
     /**
      * Calculate the amount of top padding needed (if any) to make sure that the
-     * [initialCenterItemIndex] item can be placed in the center of the viewport at
-     * [initialCenterItemScrollOffset]
+     * [AutoCenteringParams.itemIndex] item can be placed in the center of the viewport at
+     * [AutoCenteringParams.itemOffset]
      */
     private fun calculateTopAutoCenteringPaddingPx(
         visibleItems: List<ScalingLazyListItemInfo>,
         totalItemCount: Int
     ): Int {
-        if (! autoCentering.value!! || visibleItems.isEmpty() ||
+        if (autoCentering.value == null || visibleItems.isEmpty() ||
             visibleItems.first().index != 0) return 0
 
         // Work out the index we want to find - if there are less items in the list than would be
         // needed to make initialItemIndex be visible then use the last visible item
-        val itemIndexToFind = initialCenterItemIndex.coerceAtMost(totalItemCount - 1)
+        val itemIndexToFind = autoCentering.value!!.itemIndex.coerceAtMost(totalItemCount - 1)
 
         // Find the initialCenterItem, if it is null that means it is not in view - therefore
         // we have more than enough content before it to make sure it can be scrolled to the center
@@ -534,22 +525,22 @@
 
     /**
      * Calculate the amount of top padding needed (if any) to make sure that the
-     * [initialCenterItemIndex] item can be placed in the center of the viewport at
-     * [initialCenterItemScrollOffset]
+     * [AutoCenteringParams.itemIndex] item can be placed in the center of the viewport at
+     * [AutoCenteringParams.itemOffset]
      */
     private fun calculateTopAutoCenteringPaddingFromLazyListItemInfo(
         visibleItems: List<LazyListItemInfo>,
         totalItemCount: Int
     ): Int {
         // Check is list is empty or we are not at the start of the visible items
-        if (visibleItems.isEmpty() || visibleItems.isEmpty() ||
+        if (autoCentering.value == null || visibleItems.isEmpty() ||
             visibleItems[0].index != 0) return 0
 
         // Work out the index we want to find - if there are less items in the list than would be
         // needed to make initialItemIndex be visible then use the last visible item. The -3 is to
         // allow for the spacers, i.e. an underlying list of size 3 has 2 spacers in index 0 and 2
         // and one real item in index 1.
-        val itemIndexToFind = (initialCenterItemIndex + 1).coerceAtMost(totalItemCount - 3)
+        val itemIndexToFind = (autoCentering.value!!.itemIndex + 1).coerceAtMost(totalItemCount - 3)
 
         // Find the initialCenterItem, if it is null that means it is not in view - therefore
         // we have more than enough content before it to make sure it can be scrolled to the center
@@ -578,12 +569,12 @@
         } else {
             viewportHeightPx.value!! / 2f -
                 unadjustedSize / 2f
-        } - gapBetweenItemsPx.value!! - initialCenterItemScrollOffset
+        } - gapBetweenItemsPx.value!! - autoCentering.value!!.itemOffset
 
     private fun calculateBottomAutoCenteringPaddingPx(
         visibleItemsInfo: List<ScalingLazyListItemInfo>,
         totalItemsCount: Int
-    ) = if (autoCentering.value!! && visibleItemsInfo.isNotEmpty() &&
+    ) = if (autoCentering.value != null && visibleItemsInfo.isNotEmpty() &&
         visibleItemsInfo.last().index == totalItemsCount - 1
     ) {
         if (anchorType.value == ScalingLazyListAnchorType.ItemStart) {
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
index 22075d5..824eee3 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
@@ -324,7 +324,7 @@
      */
     public suspend fun snapTo(targetValue: SwipeToDismissValue) = swipeableState.snapTo(targetValue)
 
-    companion object {
+    private companion object {
         private fun <T> SwipeableState<T>.edgeNestedScrollConnection(
             edgeTouched: State<Boolean>
         ): NestedScrollConnection =
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
index aa03896..2eb89b8 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
@@ -30,8 +30,10 @@
 import androidx.compose.ui.unit.dp
 import androidx.wear.compose.foundation.ArcPaddingValues
 import androidx.wear.compose.foundation.CurvedLayout
+import androidx.wear.compose.foundation.CurvedModifier
 import androidx.wear.compose.foundation.CurvedScope
 import androidx.wear.compose.foundation.CurvedTextStyle
+import androidx.wear.compose.foundation.padding
 import androidx.wear.compose.material.TimeTextDefaults.CurvedTextSeparator
 import androidx.wear.compose.material.TimeTextDefaults.TextSeparator
 import androidx.wear.compose.material.TimeTextDefaults.timeFormat
@@ -209,8 +211,8 @@
     ) {
         curvedText(
             text = "·",
-            contentArcPadding = contentArcPadding,
-            style = curvedTextStyle
+            style = curvedTextStyle,
+            modifier = CurvedModifier.padding(contentArcPadding)
         )
     }
 
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/dialog/Dialog.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/dialog/Dialog.kt
index 323f922..fac57f8 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/dialog/Dialog.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/dialog/Dialog.kt
@@ -382,7 +382,7 @@
 ) {
     ScalingLazyColumn(
         state = scrollState,
-        autoCentering = false,
+        autoCentering = null,
         horizontalAlignment = Alignment.CenterHorizontally,
         verticalArrangement = verticalArrangement,
         contentPadding = contentPadding,
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 550fa41..1aa204d 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
         applicationId "androidx.wear.compose.integration.demos"
         minSdk 25
         targetSdk 30
-        versionCode 5
-        versionName "1.4"
+        versionCode 6
+        versionName "1.5"
         // Change the APK name to match the *testapp regex we use to pick up APKs for testing as
         // part of CI.
         archivesBaseName = "wear-compose-demos-testapp"
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CurvedLayoutDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CurvedLayoutDemo.kt
index 42f434d..89a1b35 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CurvedLayoutDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CurvedLayoutDemo.kt
@@ -39,7 +39,6 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.wear.compose.foundation.AnchorType
-import androidx.wear.compose.foundation.ArcPaddingValues
 import androidx.wear.compose.foundation.CurvedAlignment
 import androidx.wear.compose.foundation.CurvedDirection
 import androidx.wear.compose.foundation.CurvedLayout
@@ -50,6 +49,7 @@
 import androidx.wear.compose.foundation.curvedColumn
 import androidx.wear.compose.foundation.curvedComposable
 import androidx.wear.compose.foundation.curvedRow
+import androidx.wear.compose.foundation.padding
 import androidx.wear.compose.foundation.sizeIn
 import androidx.wear.compose.foundation.weight
 import androidx.wear.compose.material.Text
@@ -229,7 +229,7 @@
                 fontSize = 24.sp
             ),
             angularDirection = CurvedDirection.Angular.Reversed,
-            contentArcPadding = ArcPaddingValues(angular = 5.dp),
+            modifier = CurvedModifier.padding(angular = 5.dp),
             // TODO: Re-add when we implement alignment modifiers.
             // modifier = Modifier.radialAlignment(RadialAlignment.Inner)
         )
@@ -281,7 +281,7 @@
                         CurvedTextStyle(
                             fontSize = 24.sp
                         ),
-                        contentArcPadding = ArcPaddingValues(angular = 5.dp),
+                        modifier = CurvedModifier.padding(angular = 5.dp),
                     )
                     curvedColumn {
                         repeat(3) {
@@ -291,11 +291,11 @@
                     curvedRow {
                         basicCurvedText(
                             "after",
-                            contentArcPadding = ArcPaddingValues(angular = 4.dp)
+                            modifier = CurvedModifier.padding(angular = 4.dp),
                         )
                         basicCurvedText(
                             "end",
-                            contentArcPadding = ArcPaddingValues(angular = 4.dp)
+                            modifier = CurvedModifier.padding(angular = 4.dp),
                         )
                     }
                 }
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PositionIndicatorDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PositionIndicatorDemos.kt
index 9da029a..76fe1ef 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PositionIndicatorDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PositionIndicatorDemos.kt
@@ -93,7 +93,7 @@
     ) {
         ScalingLazyColumn(
             state = listState,
-            autoCentering = false
+            autoCentering = null
         ) {
             items(
                 count = if (smallList) 3 else 10
diff --git a/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/BaselineActivity.kt b/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/BaselineActivity.kt
index ea02b8a..756bf60 100644
--- a/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/BaselineActivity.kt
+++ b/wear/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/wear/compose/integration/macrobenchmark/target/BaselineActivity.kt
@@ -41,10 +41,11 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.navigation.NavHostController
-import androidx.wear.compose.foundation.ArcPaddingValues
 import androidx.wear.compose.foundation.CurvedLayout
+import androidx.wear.compose.foundation.CurvedModifier
 import androidx.wear.compose.foundation.CurvedTextStyle
 import androidx.wear.compose.foundation.basicCurvedText
+import androidx.wear.compose.foundation.padding
 import androidx.wear.compose.material.AppCard
 import androidx.wear.compose.material.Button
 import androidx.wear.compose.material.Card
@@ -238,7 +239,7 @@
                 color = Color.White,
                 background = background
             ),
-            contentArcPadding = ArcPaddingValues(2.dp)
+            modifier = CurvedModifier.padding(2.dp)
         )
     }
     CurvedLayout(anchor = 310f) {
diff --git a/wear/tiles/tiles-material/build.gradle b/wear/tiles/tiles-material/build.gradle
index b03ceb2..a72bfba 100644
--- a/wear/tiles/tiles-material/build.gradle
+++ b/wear/tiles/tiles-material/build.gradle
@@ -18,22 +18,43 @@
 
 plugins {
     id("AndroidXPlugin")
+    id("kotlin-android")
     id("com.android.library")
+    id("com.google.protobuf")
 }
 
 dependencies {
     api("androidx.annotation:annotation:1.2.0")
-    api(project(":wear:tiles:tiles"))
-
+    implementation(project(":wear:tiles:tiles"))
     implementation(project(":wear:tiles:tiles-proto"))
 
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation("androidx.core:core:1.7.0")
+    androidTestImplementation(project(":test:screenshot:screenshot"))
+    androidTestImplementation(project(":wear:tiles:tiles-renderer"))
+    androidTestRuntimeOnly(project(path: ":wear:tiles:tiles-proto", configuration: "shadow"))
+    androidTestImplementation("com.google.protobuf:protobuf-java:3.10.0")
+
     annotationProcessor(libs.nullaway)
 }
 
 android {
     defaultConfig {
-        minSdkVersion 25
+        minSdkVersion 26
     }
+
+    sourceSets {
+        androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/wear/wear-tiles-material"
+    }
+
+    defaultConfig {
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
     namespace "androidx.wear.tiles.material"
 }
 
diff --git a/wear/tiles/tiles-material/src/androidTest/AndroidManifest.xml b/wear/tiles/tiles-material/src/androidTest/AndroidManifest.xml
index 4d68dc2..55cb77e 100644
--- a/wear/tiles/tiles-material/src/androidTest/AndroidManifest.xml
+++ b/wear/tiles/tiles-material/src/androidTest/AndroidManifest.xml
@@ -14,6 +14,21 @@
   See the License for the specific language governing permissions and
   limitations under the License.
   -->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="androidx.wear.tiles.material">
+    <application
+        android:label="Golden Tests"
+        android:supportsRtl="true"
+        android:theme="@android:style/Theme.DeviceDefault"
+        android:taskAffinity="">
+        <uses-library android:name="android.test.runner" />
 
+        <activity android:name="androidx.wear.tiles.material.testapp.GoldenTestActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
 </manifest>
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenTest.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenTest.java
new file mode 100644
index 0000000..3022faf
--- /dev/null
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenTest.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.material;
+
+import static androidx.wear.tiles.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.tiles.material.RunnerUtils.SCREEN_WIDTH;
+import static androidx.wear.tiles.material.RunnerUtils.runSingleScreenshotTest;
+import static androidx.wear.tiles.material.TestCasesGenerator.generateTestCases;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+
+import androidx.annotation.Dimension;
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.wear.tiles.DeviceParametersBuilders;
+import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.tiles.LayoutElementBuilders.LayoutElement;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.stream.Collectors;
+
+@RunWith(Parameterized.class)
+@LargeTest
+public class MaterialGoldenTest {
+    private final LayoutElement mLayoutElement;
+    private final String mExpected;
+
+    @Rule
+    public AndroidXScreenshotTestRule mScreenshotRule =
+            new AndroidXScreenshotTestRule("wear/wear-tiles-material");
+
+    public MaterialGoldenTest(LayoutElement layoutElement, String expected) {
+        mLayoutElement = layoutElement;
+        mExpected = expected;
+    }
+
+    @Dimension(unit = Dimension.DP)
+    static int pxToDp(int px, float scale) {
+        return (int) ((px - 0.5f) / scale);
+    }
+
+    @Parameterized.Parameters(name = "{1}")
+    public static Collection<Object[]> data() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+        float scale = displayMetrics.density;
+
+        InstrumentationRegistry.getInstrumentation()
+                .getContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+        InstrumentationRegistry.getInstrumentation()
+                .getTargetContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+
+
+        DeviceParameters deviceParameters =
+                new DeviceParameters.Builder()
+                        .setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
+                        .setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
+                        .setScreenDensity(displayMetrics.density)
+                        // Not important for components.
+                        .setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
+                        .build();
+
+        HashMap<LayoutElement, String> testCases =
+                generateTestCases(context, deviceParameters, "");
+
+        return testCases.entrySet()
+                .stream()
+                .map(test -> new Object[]{test.getKey(), test.getValue()})
+                .collect(Collectors.toList());
+    }
+
+    @Test
+    public void test() {
+        runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
+    }
+}
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java
new file mode 100644
index 0000000..25b116e
--- /dev/null
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/MaterialGoldenXLTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.material;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static androidx.wear.tiles.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.tiles.material.RunnerUtils.SCREEN_WIDTH;
+import static androidx.wear.tiles.material.RunnerUtils.runSingleScreenshotTest;
+import static androidx.wear.tiles.material.TestCasesGenerator.XXXL_SCALE_SUFFIX;
+import static androidx.wear.tiles.material.TestCasesGenerator.generateTestCases;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+
+import androidx.annotation.Dimension;
+import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.wear.tiles.DeviceParametersBuilders;
+import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.tiles.LayoutElementBuilders.LayoutElement;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.stream.Collectors;
+
+@RunWith(Parameterized.class)
+@LargeTest
+public class MaterialGoldenXLTest {
+    /* We set DisplayMetrics in the data() method for creating test cases. However, when running all
+    tests together, first all parametrization (data()) methods are called, and then individual
+    tests, causing that actual DisplayMetrics will be different. So we need to restore it before
+    each test. */
+    private static final DisplayMetrics DISPLAY_METRICS_FOR_TEST = new DisplayMetrics();
+    private static final DisplayMetrics OLD_DISPLAY_METRICS = new DisplayMetrics();
+
+    private static final float FONT_SCALE_XXXL = 1.24f;
+
+    private final LayoutElement mLayoutElement;
+    private final String mExpected;
+
+    @Rule
+    public AndroidXScreenshotTestRule mScreenshotRule =
+            new AndroidXScreenshotTestRule("wear/wear-tiles-material");
+
+    public MaterialGoldenXLTest(LayoutElement layoutElement, String expected) {
+        mLayoutElement = layoutElement;
+        mExpected = expected;
+    }
+
+    @Dimension(unit = Dimension.DP)
+    static int pxToDp(int px, float scale) {
+        return (int) ((px - 0.5f) / scale);
+    }
+
+    @Parameterized.Parameters(name = "{1}")
+    public static Collection<Object[]> data() {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        DisplayMetrics currentDisplayMetrics = new DisplayMetrics();
+        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+        currentDisplayMetrics.setTo(displayMetrics);
+        displayMetrics.scaledDensity *= FONT_SCALE_XXXL;
+
+        InstrumentationRegistry.getInstrumentation()
+                .getContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+        InstrumentationRegistry.getInstrumentation()
+                .getTargetContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+
+        DISPLAY_METRICS_FOR_TEST.setTo(displayMetrics);
+
+        float scale = displayMetrics.density;
+        DeviceParameters deviceParameters =
+                new DeviceParameters.Builder()
+                        .setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
+                        .setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
+                        .setScreenDensity(displayMetrics.density)
+                        // Not important for components.
+                        .setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
+                        .build();
+
+        HashMap<LayoutElement, String> testCases =
+                generateTestCases(context, deviceParameters, XXXL_SCALE_SUFFIX);
+
+        // Restore state before this method, so other test have correct context.
+        InstrumentationRegistry.getInstrumentation()
+                .getContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(currentDisplayMetrics);
+        InstrumentationRegistry.getInstrumentation()
+                .getTargetContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(currentDisplayMetrics);
+
+        return testCases.entrySet()
+                .stream()
+                .map(test -> new Object[]{test.getKey(), test.getValue()})
+                .collect(Collectors.toList());
+    }
+
+    @Parameterized.BeforeParam
+    public static void restoreBefore() {
+        OLD_DISPLAY_METRICS.setTo(getApplicationContext().getResources().getDisplayMetrics());
+        getApplicationContext().getResources().getDisplayMetrics().setTo(DISPLAY_METRICS_FOR_TEST);
+    }
+
+    @Parameterized.AfterParam
+    public static void restoreAfter() {
+        getApplicationContext().getResources().getDisplayMetrics().setTo(OLD_DISPLAY_METRICS);
+    }
+
+    @Test
+    public void test() {
+        runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
+    }
+}
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/RunnerUtils.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/RunnerUtils.java
new file mode 100644
index 0000000..f707b14
--- /dev/null
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/RunnerUtils.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.material;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.test.screenshot.matchers.MSSIMMatcher;
+import androidx.wear.tiles.LayoutElementBuilders;
+import androidx.wear.tiles.material.testapp.GoldenTestActivity;
+
+public class RunnerUtils {
+    // This isn't totally ideal right now. The screenshot tests run on a phone, so emulate some
+    // watch dimensions here.
+    public static final int SCREEN_WIDTH = 390;
+    public static final int SCREEN_HEIGHT = 390;
+
+    private RunnerUtils() {}
+
+    static void runSingleScreenshotTest(@NonNull AndroidXScreenshotTestRule rule,
+            @NonNull LayoutElementBuilders.LayoutElement layoutElement,
+            @NonNull String expected) {
+        byte[] layoutElementPayload = layoutElement.toLayoutElementProto().toByteArray();
+
+        Intent startIntent =
+                new Intent(
+                        InstrumentationRegistry.getInstrumentation().getTargetContext(),
+                        GoldenTestActivity.class);
+        startIntent.putExtra("layout", layoutElementPayload);
+
+        ActivityScenario<GoldenTestActivity> scenario = ActivityScenario.launch(startIntent);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        try {
+            // Wait 1s after launching the activity. This allows for the old white layout in the
+            // bootstrap activity to fully go away before proceeding.
+            Thread.sleep(1000);
+        } catch (Exception ex) {
+            if (ex instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+            }
+            Log.e("MaterialGoldenTest", "Error sleeping", ex);
+        }
+
+        Bitmap bitmap = Bitmap.createBitmap(
+                InstrumentationRegistry.getInstrumentation().getUiAutomation().takeScreenshot(),
+                0, 0, SCREEN_WIDTH, SCREEN_HEIGHT
+        );
+        rule.assertBitmapAgainstGolden(bitmap, expected, new MSSIMMatcher());
+
+        // There's a weird bug (related to b/159805732) where, when calling .close() on
+        // ActivityScenario or calling finish() and immediately exiting the test, the test can hang
+        // on a white screen for 45s. Closing the activity here and waiting for 1s seems to fix
+        // this.
+        scenario.onActivity(Activity::finish);
+
+        try {
+            Thread.sleep(1000);
+        } catch (Exception ex) {
+            if (ex instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+            }
+            Log.e("MaterialGoldenTest", "Error sleeping", ex);
+        }
+    }
+}
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/TestCasesGenerator.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/TestCasesGenerator.java
new file mode 100644
index 0000000..60f61f3
--- /dev/null
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/TestCasesGenerator.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.material;
+
+import static androidx.wear.tiles.ColorBuilders.argb;
+import static androidx.wear.tiles.DimensionBuilders.dp;
+import static androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER;
+import static androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_END;
+import static androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_START;
+import static androidx.wear.tiles.material.ProgressIndicatorDefaults.GAP_END_ANGLE;
+import static androidx.wear.tiles.material.ProgressIndicatorDefaults.GAP_START_ANGLE;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import androidx.annotation.NonNull;
+import androidx.wear.tiles.ActionBuilders.LaunchAction;
+import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.tiles.LayoutElementBuilders;
+import androidx.wear.tiles.LayoutElementBuilders.LayoutElement;
+import androidx.wear.tiles.ModifiersBuilders.Clickable;
+
+import java.util.HashMap;
+
+public class TestCasesGenerator {
+    private TestCasesGenerator() {}
+
+    private static final String ICON_ID = "tile_icon";
+    private static final String AVATAR = "avatar_image";
+    public static final String NORMAL_SCALE_SUFFIX = "";
+    public static final String XXXL_SCALE_SUFFIX = "_xxxl";
+
+    /**
+     * This function will append goldenSuffix on the name of the golden images that should be
+     * different for different user font sizes. Note that some of the golden will have the same name
+     * as it should point on the same size independent image.
+     */
+    @NonNull
+    static HashMap<LayoutElement, String> generateTestCases(
+            @NonNull Context context,
+            @NonNull DeviceParameters deviceParameters,
+            @NonNull String goldenSuffix) {
+        Clickable clickable =
+                new Clickable.Builder()
+                        .setOnClick(new LaunchAction.Builder().build())
+                        .setId("action_id")
+                        .build();
+        String mainText = "Primary label";
+        String labelText = "Secondary label";
+        String largeChipText = "Action";
+        HashMap<LayoutElement, String> testCases = new HashMap<>();
+
+        testCases.put(
+                new Button.Builder(context, clickable).setIconContent(ICON_ID).build(),
+                "default_icon_button_golden" + NORMAL_SCALE_SUFFIX);
+        testCases.put(
+                new Button.Builder(context, clickable)
+                        .setButtonColors(ButtonDefaults.SECONDARY_BUTTON_COLORS)
+                        .setIconContent(ICON_ID)
+                        .setSize(ButtonDefaults.EXTRA_LARGE_BUTTON_SIZE)
+                        .build(),
+                "extralarge_secondary_icon_after_button_golden" + NORMAL_SCALE_SUFFIX);
+        testCases.put(
+                new Button.Builder(context, clickable)
+                        .setSize(ButtonDefaults.LARGE_BUTTON_SIZE)
+                        .setButtonColors(ButtonDefaults.SECONDARY_BUTTON_COLORS)
+                        .setIconContent(ICON_ID, dp(40))
+                        .build(),
+                "large_secondary_icon_40size_button_golden" + NORMAL_SCALE_SUFFIX);
+        testCases.put(
+                new Button.Builder(context, clickable)
+                        .setButtonColors(new ButtonColors(Color.YELLOW, Color.GREEN))
+                        .setSize(ButtonDefaults.EXTRA_LARGE_BUTTON_SIZE)
+                        .setContent(
+                                new Text.Builder(context, "ABC")
+                                        .setTypography(Typography.TYPOGRAPHY_DISPLAY1)
+                                        .setItalic(true)
+                                        .setColor(argb(Color.GREEN))
+                                        .build())
+                        .build(),
+                "extralarge_custom_text_custom_sizefont_button_golden" + goldenSuffix);
+        testCases.put(
+                new Button.Builder(context, clickable).setTextContent("ABC").build(),
+                "default_text_button_golden" + goldenSuffix);
+        testCases.put(
+                new Button.Builder(context, clickable).setImageContent(AVATAR).build(),
+                "default_image_button_golden" + NORMAL_SCALE_SUFFIX);
+
+        testCases.put(
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setPrimaryTextContent(mainText)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+                        .build(),
+                "default_chip_maintext_golden" + goldenSuffix);
+        testCases.put(
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setPrimaryTextLabelContent(mainText, labelText)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+                        .build(),
+                "default_chip_maintextlabeltext_golden" + goldenSuffix);
+        testCases.put(
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setPrimaryTextIconContent(mainText, ICON_ID)
+                        .build(),
+                "default_chip_maintexticon_golden" + goldenSuffix);
+        testCases.put(
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+                        .setChipColors(ChipDefaults.SECONDARY_COLORS)
+                        .setPrimaryTextContent(mainText)
+                        .build(),
+                "secondary_chip_maintext_centered_golden" + goldenSuffix);
+        testCases.put(
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setWidth(130)
+                        .setPrimaryTextLabelIconContent(mainText, labelText, ICON_ID)
+                        .setChipColors(
+                                new ChipColors(Color.YELLOW, Color.GREEN, Color.BLACK, Color.GRAY))
+                        .build(),
+                "custom_chip_all_overflows_golden" + goldenSuffix);
+        testCases.put(
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+                        .setPrimaryTextLabelIconContent(mainText, labelText, ICON_ID)
+                        .build(),
+                "default_chip_all_centered_golden" + goldenSuffix);
+        testCases.put(
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_END)
+                        .setPrimaryTextLabelIconContent(mainText, labelText, ICON_ID)
+                        .build(),
+                "default_chip_all_rigthalign_golden" + goldenSuffix);
+        testCases.put(
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setPrimaryTextIconContent(mainText, ICON_ID)
+                        .setWidth(150)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+                        .setChipColors(
+                                new ChipColors(Color.YELLOW, Color.GREEN, Color.BLACK, Color.GRAY))
+                        .build(),
+                "custom_chip_icon_primary_overflows_golden" + goldenSuffix);
+        testCases.put(
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+                        .setContent(
+                                new LayoutElementBuilders.Box.Builder()
+                                        .addContent(
+                                                new Text.Builder(context, "random text")
+                                                        .setTypography(Typography.TYPOGRAPHY_TITLE3)
+                                                        .setItalic(true)
+                                                        .build())
+                                        .build())
+                        .build(),
+                "chip_custom_content_centered_golden" + goldenSuffix);
+        testCases.put(
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setChipColors(ChipDefaults.SECONDARY_COLORS)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+                        .setContent(
+                                new LayoutElementBuilders.Row.Builder()
+                                        .addContent(
+                                                new Text.Builder(context, "text1")
+                                                        .setTypography(Typography.TYPOGRAPHY_TITLE3)
+                                                        .setItalic(true)
+                                                        .setColor(argb(Color.WHITE))
+                                                        .build())
+                                        .addContent(
+                                                new Text.Builder(context, "text2")
+                                                        .setTypography(Typography.TYPOGRAPHY_TITLE2)
+                                                        .setColor(argb(Color.YELLOW))
+                                                        .build())
+                                        .build())
+                        .setWidth(150)
+                        .build(),
+                "chip_custom_content_leftaligned_golden" + goldenSuffix);
+        testCases.put(
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setPrimaryTextContent("abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde")
+                        .build(),
+                "chip_2lines_primary_overflows_golden" + goldenSuffix);
+
+        // Different text lengths to test expanding the width based on the size of text. If it's
+        // more than 9, the rest will be deleted.
+        testCases.put(
+                new CompactChip.Builder(context, "Ab", clickable, deviceParameters).build(),
+                "compactchip_default_len2_golden" + goldenSuffix);
+        testCases.put(
+                new CompactChip.Builder(context, "Abcde", clickable, deviceParameters).build(),
+                "compactchip_default_len5_golden" + goldenSuffix);
+        testCases.put(
+                new CompactChip.Builder(context, "Abcdefghi", clickable, deviceParameters).build(),
+                "compactchip_default_len9_golden" + goldenSuffix);
+        testCases.put(
+                new CompactChip.Builder(context, "AbcdefghiEXTRA", clickable,
+                        deviceParameters).build(),
+                "compactchip_default_toolong_golden" + goldenSuffix);
+        testCases.put(
+                new CompactChip.Builder(context, "Action", clickable, deviceParameters)
+                        .setChipColors(new ChipColors(Color.YELLOW, Color.BLACK))
+                        .build(),
+                "compactchip_custom_default_golden" + goldenSuffix);
+
+        testCases.put(
+                new TitleChip.Builder(context, largeChipText, clickable, deviceParameters).build(),
+                "titlechip_default_golden" + goldenSuffix);
+        testCases.put(
+                new TitleChip.Builder(context, "abcdeabcdeabcdeEXTRA", clickable,
+                        deviceParameters).build(),
+                "titlechip_default_texttoolong_golden" + goldenSuffix);
+        testCases.put(
+                new TitleChip.Builder(context, largeChipText, clickable, deviceParameters)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+                        .setChipColors(ChipDefaults.TITLE_SECONDARY_COLORS)
+                        .build(),
+                "titlechip_leftalign_secondary_default_golden" + goldenSuffix);
+        testCases.put(
+                new TitleChip.Builder(context, largeChipText, clickable, deviceParameters)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+                        .setChipColors(new ChipColors(Color.YELLOW, Color.BLUE))
+                        .setWidth(150)
+                        .build(),
+                "titlechip_centered_custom_150_secondary_default_golden" + goldenSuffix);
+
+        testCases.put(
+                new CircularProgressIndicator.Builder().build(),
+                "default_full_circularprogressindicator");
+        testCases.put(
+                new CircularProgressIndicator.Builder()
+                        .setStartAngle(GAP_START_ANGLE)
+                        .setEndAngle(GAP_END_ANGLE)
+                        .build(),
+                "default_gap_circularprogressindicator");
+        testCases.put(
+                new CircularProgressIndicator.Builder().setProgress(0.25f).build(),
+                "default_full_90_circularprogressindicator");
+        testCases.put(
+                new CircularProgressIndicator.Builder()
+                        .setProgress(0.25f)
+                        .setStartAngle(GAP_START_ANGLE)
+                        .setEndAngle(GAP_END_ANGLE)
+                        .build(),
+                "default_gap_90_circularprogressindicator");
+        testCases.put(
+                new CircularProgressIndicator.Builder()
+                        .setStartAngle(45)
+                        .setEndAngle(270)
+                        .setProgress(0.2f)
+                        .setStrokeWidth(12)
+                        .setCircularProgressIndicatorColors(
+                                new ProgressIndicatorColors(Color.BLUE, Color.YELLOW))
+                        .build(),
+                "custom_gap_45_circularprogressindicator");
+
+        testCases.put(
+                new Text.Builder(context, "Testing").build(),
+                "default_text_golden" + goldenSuffix);
+        testCases.put(
+                new Text.Builder(context, "Testing text.")
+                        .setItalic(true)
+                        .setColor(argb(Color.YELLOW))
+                        .setTypography(Typography.TYPOGRAPHY_BODY2)
+                        .build(),
+                "custom_text_golden" + goldenSuffix);
+        testCases.put(
+                new Text.Builder(context, "abcdeabcdeabcde").build(),
+                "overflow_text_golden" + goldenSuffix);
+
+        return testCases;
+    }
+}
diff --git a/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/testapp/GoldenTestActivity.java b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/testapp/GoldenTestActivity.java
new file mode 100644
index 0000000..f775a6f
--- /dev/null
+++ b/wear/tiles/tiles-material/src/androidTest/java/androidx/wear/tiles/material/testapp/GoldenTestActivity.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.tiles.material.testapp;
+
+import static androidx.wear.tiles.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.tiles.material.RunnerUtils.SCREEN_WIDTH;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.wear.tiles.LayoutElementBuilders;
+import androidx.wear.tiles.LayoutElementBuilders.Layout;
+import androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId;
+import androidx.wear.tiles.ResourceBuilders.ImageResource;
+import androidx.wear.tiles.ResourceBuilders.Resources;
+import androidx.wear.tiles.material.R;
+import androidx.wear.tiles.proto.LayoutElementProto.LayoutElement;
+import androidx.wear.tiles.renderer.TileRenderer;
+
+import java.util.concurrent.Executor;
+
+
+public class GoldenTestActivity extends Activity {
+    private static final String ICON_ID = "tile_icon";
+    private static final String AVATAR = "avatar_image";
+
+    @Override
+    @SuppressWarnings("deprecation")
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        byte[] layoutPayload = getIntent().getExtras().getByteArray("layout");
+
+        LayoutElement layoutElementProto;
+        try {
+            layoutElementProto =
+                    LayoutElement.parseFrom(layoutPayload);
+        } catch (Exception ex) {
+            // It's a test, just rethrow.
+            throw new IllegalArgumentException("Could not deserialize layout proto", ex);
+        }
+
+        LayoutElementBuilders.LayoutElement rootLayoutElement =
+                LayoutElementBuilders.LayoutElement.fromLayoutElementProto(layoutElementProto);
+
+        Context appContext = getApplicationContext();
+        FrameLayout root = new FrameLayout(appContext);
+        root.setLayoutParams(new LayoutParams(SCREEN_WIDTH, SCREEN_HEIGHT));
+
+        Layout layout = new Layout.Builder().setRoot(rootLayoutElement).build();
+
+        Executor mainExecutor = ContextCompat.getMainExecutor(getApplicationContext());
+
+        Resources resources = generateResources();
+        TileRenderer renderer =
+                new TileRenderer(
+                        appContext,
+                        layout,
+                        resources,
+                        mainExecutor,
+                        i -> {});
+
+        View firstChild = renderer.inflate(root);
+
+        // Simulate what the thing outside the renderer should do. Center the contents.
+        LayoutParams layoutParams = (LayoutParams) firstChild.getLayoutParams();
+        layoutParams.gravity = Gravity.CENTER;
+
+        // Set the activity to be full screen so when we crop the Bitmap we don't get time bar etc.
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+                WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+        setContentView(root, new ViewGroup.LayoutParams(SCREEN_WIDTH, SCREEN_HEIGHT));
+        super.onCreate(savedInstanceState);
+    }
+
+    private static Resources generateResources() {
+        return new Resources.Builder()
+                .addIdToImageMapping(
+                        ICON_ID,
+                        new ImageResource.Builder()
+                                .setAndroidResourceByResId(
+                                        new AndroidImageResourceByResId.Builder()
+                                                .setResourceId(R.drawable.tile_icon)
+                                                .build())
+                                .build())
+                .addIdToImageMapping(
+                        AVATAR,
+                        new ImageResource.Builder()
+                                .setAndroidResourceByResId(
+                                        new AndroidImageResourceByResId.Builder()
+                                                .setResourceId(R.drawable.avatar)
+                                                .build())
+                                .build())
+                .build();
+    }
+}
+
diff --git a/wear/tiles/tiles-material/src/main/res/drawable/avatar.png b/wear/tiles/tiles-material/src/main/res/drawable/avatar.png
new file mode 100644
index 0000000..a6da988
--- /dev/null
+++ b/wear/tiles/tiles-material/src/main/res/drawable/avatar.png
Binary files differ
diff --git a/wear/tiles/tiles-material/src/main/res/drawable/tile_icon.xml b/wear/tiles/tiles-material/src/main/res/drawable/tile_icon.xml
new file mode 100644
index 0000000..21eb853
--- /dev/null
+++ b/wear/tiles/tiles-material/src/main/res/drawable/tile_icon.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:fillColor="#000"
+      android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
+</vector>
diff --git a/wear/watchface/watchface-client/api/1.1.0-beta01.txt b/wear/watchface/watchface-client/api/1.1.0-beta01.txt
index 6e77b4d..6937121 100644
--- a/wear/watchface/watchface-client/api/1.1.0-beta01.txt
+++ b/wear/watchface/watchface-client/api/1.1.0-beta01.txt
@@ -203,19 +203,6 @@
     ctor public WatchFaceControlClient.ServiceStartFailureException(optional String message);
   }
 
-  public final class WatchFaceException extends java.lang.Exception {
-    ctor public WatchFaceException(Exception e, int reason);
-    method public int getReason();
-    property public final int reason;
-    field public static final androidx.wear.watchface.client.WatchFaceException.Companion Companion;
-    field public static final int TRANSACTION_TOO_LARGE = 2; // 0x2
-    field public static final int UNKNOWN = 3; // 0x3
-    field public static final int WATCHFACE_DIED = 1; // 0x1
-  }
-
-  public static final class WatchFaceException.Companion {
-  }
-
   public final class WatchFaceExceptionKt {
   }
 
@@ -226,8 +213,8 @@
   }
 
   public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
-    method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws androidx.wear.watchface.client.WatchFaceException;
-    method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws androidx.wear.watchface.client.WatchFaceException;
+    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
+    method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public boolean isUserStyleSchemaStatic();
     property public abstract boolean isUserStyleSchemaStatic;
     field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
diff --git a/wear/watchface/watchface-client/api/current.txt b/wear/watchface/watchface-client/api/current.txt
index 6e77b4d..6937121 100644
--- a/wear/watchface/watchface-client/api/current.txt
+++ b/wear/watchface/watchface-client/api/current.txt
@@ -203,19 +203,6 @@
     ctor public WatchFaceControlClient.ServiceStartFailureException(optional String message);
   }
 
-  public final class WatchFaceException extends java.lang.Exception {
-    ctor public WatchFaceException(Exception e, int reason);
-    method public int getReason();
-    property public final int reason;
-    field public static final androidx.wear.watchface.client.WatchFaceException.Companion Companion;
-    field public static final int TRANSACTION_TOO_LARGE = 2; // 0x2
-    field public static final int UNKNOWN = 3; // 0x3
-    field public static final int WATCHFACE_DIED = 1; // 0x1
-  }
-
-  public static final class WatchFaceException.Companion {
-  }
-
   public final class WatchFaceExceptionKt {
   }
 
@@ -226,8 +213,8 @@
   }
 
   public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
-    method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws androidx.wear.watchface.client.WatchFaceException;
-    method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws androidx.wear.watchface.client.WatchFaceException;
+    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
+    method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public boolean isUserStyleSchemaStatic();
     property public abstract boolean isUserStyleSchemaStatic;
     field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
diff --git a/wear/watchface/watchface-client/api/public_plus_experimental_1.1.0-beta01.txt b/wear/watchface/watchface-client/api/public_plus_experimental_1.1.0-beta01.txt
index 95ff68c..92202fd 100644
--- a/wear/watchface/watchface-client/api/public_plus_experimental_1.1.0-beta01.txt
+++ b/wear/watchface/watchface-client/api/public_plus_experimental_1.1.0-beta01.txt
@@ -102,7 +102,7 @@
     method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState();
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant();
-    method @androidx.wear.watchface.WatchFaceFlavorsExperimental @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public default androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors() throws androidx.wear.watchface.client.WatchFaceException;
+    method @androidx.wear.watchface.WatchFaceFlavorsExperimental public default androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors();
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default byte[] getUserStyleSchemaDigestHash() throws android.os.RemoteException;
     method @AnyThread public boolean isConnectionAlive();
@@ -207,19 +207,6 @@
     ctor public WatchFaceControlClient.ServiceStartFailureException(optional String message);
   }
 
-  public final class WatchFaceException extends java.lang.Exception {
-    ctor public WatchFaceException(Exception e, int reason);
-    method public int getReason();
-    property public final int reason;
-    field public static final androidx.wear.watchface.client.WatchFaceException.Companion Companion;
-    field public static final int TRANSACTION_TOO_LARGE = 2; // 0x2
-    field public static final int UNKNOWN = 3; // 0x3
-    field public static final int WATCHFACE_DIED = 1; // 0x1
-  }
-
-  public static final class WatchFaceException.Companion {
-  }
-
   public final class WatchFaceExceptionKt {
   }
 
@@ -230,9 +217,9 @@
   }
 
   public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
-    method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws androidx.wear.watchface.client.WatchFaceException;
-    method @androidx.wear.watchface.WatchFaceFlavorsExperimental @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors() throws androidx.wear.watchface.client.WatchFaceException;
-    method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws androidx.wear.watchface.client.WatchFaceException;
+    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
+    method @androidx.wear.watchface.WatchFaceFlavorsExperimental public androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors();
+    method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public boolean isUserStyleSchemaStatic();
     property public abstract boolean isUserStyleSchemaStatic;
     field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
diff --git a/wear/watchface/watchface-client/api/public_plus_experimental_current.txt b/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
index 95ff68c..92202fd 100644
--- a/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
@@ -102,7 +102,7 @@
     method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotState> getComplicationSlotsState();
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.time.Instant getPreviewReferenceInstant();
-    method @androidx.wear.watchface.WatchFaceFlavorsExperimental @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public default androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors() throws androidx.wear.watchface.client.WatchFaceException;
+    method @androidx.wear.watchface.WatchFaceFlavorsExperimental public default androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors();
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public default byte[] getUserStyleSchemaDigestHash() throws android.os.RemoteException;
     method @AnyThread public boolean isConnectionAlive();
@@ -207,19 +207,6 @@
     ctor public WatchFaceControlClient.ServiceStartFailureException(optional String message);
   }
 
-  public final class WatchFaceException extends java.lang.Exception {
-    ctor public WatchFaceException(Exception e, int reason);
-    method public int getReason();
-    property public final int reason;
-    field public static final androidx.wear.watchface.client.WatchFaceException.Companion Companion;
-    field public static final int TRANSACTION_TOO_LARGE = 2; // 0x2
-    field public static final int UNKNOWN = 3; // 0x3
-    field public static final int WATCHFACE_DIED = 1; // 0x1
-  }
-
-  public static final class WatchFaceException.Companion {
-  }
-
   public final class WatchFaceExceptionKt {
   }
 
@@ -230,9 +217,9 @@
   }
 
   public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
-    method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws androidx.wear.watchface.client.WatchFaceException;
-    method @androidx.wear.watchface.WatchFaceFlavorsExperimental @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors() throws androidx.wear.watchface.client.WatchFaceException;
-    method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws androidx.wear.watchface.client.WatchFaceException;
+    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
+    method @androidx.wear.watchface.WatchFaceFlavorsExperimental public androidx.wear.watchface.UserStyleFlavors getUserStyleFlavors();
+    method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public boolean isUserStyleSchemaStatic();
     property public abstract boolean isUserStyleSchemaStatic;
     field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
diff --git a/wear/watchface/watchface-client/api/restricted_1.1.0-beta01.txt b/wear/watchface/watchface-client/api/restricted_1.1.0-beta01.txt
index 6e77b4d..6937121 100644
--- a/wear/watchface/watchface-client/api/restricted_1.1.0-beta01.txt
+++ b/wear/watchface/watchface-client/api/restricted_1.1.0-beta01.txt
@@ -203,19 +203,6 @@
     ctor public WatchFaceControlClient.ServiceStartFailureException(optional String message);
   }
 
-  public final class WatchFaceException extends java.lang.Exception {
-    ctor public WatchFaceException(Exception e, int reason);
-    method public int getReason();
-    property public final int reason;
-    field public static final androidx.wear.watchface.client.WatchFaceException.Companion Companion;
-    field public static final int TRANSACTION_TOO_LARGE = 2; // 0x2
-    field public static final int UNKNOWN = 3; // 0x3
-    field public static final int WATCHFACE_DIED = 1; // 0x1
-  }
-
-  public static final class WatchFaceException.Companion {
-  }
-
   public final class WatchFaceExceptionKt {
   }
 
@@ -226,8 +213,8 @@
   }
 
   public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
-    method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws androidx.wear.watchface.client.WatchFaceException;
-    method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws androidx.wear.watchface.client.WatchFaceException;
+    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
+    method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public boolean isUserStyleSchemaStatic();
     property public abstract boolean isUserStyleSchemaStatic;
     field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
diff --git a/wear/watchface/watchface-client/api/restricted_current.txt b/wear/watchface/watchface-client/api/restricted_current.txt
index 6e77b4d..6937121 100644
--- a/wear/watchface/watchface-client/api/restricted_current.txt
+++ b/wear/watchface/watchface-client/api/restricted_current.txt
@@ -203,19 +203,6 @@
     ctor public WatchFaceControlClient.ServiceStartFailureException(optional String message);
   }
 
-  public final class WatchFaceException extends java.lang.Exception {
-    ctor public WatchFaceException(Exception e, int reason);
-    method public int getReason();
-    property public final int reason;
-    field public static final androidx.wear.watchface.client.WatchFaceException.Companion Companion;
-    field public static final int TRANSACTION_TOO_LARGE = 2; // 0x2
-    field public static final int UNKNOWN = 3; // 0x3
-    field public static final int WATCHFACE_DIED = 1; // 0x1
-  }
-
-  public static final class WatchFaceException.Companion {
-  }
-
   public final class WatchFaceExceptionKt {
   }
 
@@ -226,8 +213,8 @@
   }
 
   public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
-    method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws androidx.wear.watchface.client.WatchFaceException;
-    method @kotlin.jvm.Throws(exceptionClasses=WatchFaceException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws androidx.wear.watchface.client.WatchFaceException;
+    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
+    method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public boolean isUserStyleSchemaStatic();
     property public abstract boolean isUserStyleSchemaStatic;
     field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
diff --git a/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml b/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml
index a2ae439..4e73052 100644
--- a/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml
+++ b/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml
@@ -18,6 +18,13 @@
     <application android:requestLegacyExternalStorage="true">
         <service android:name="androidx.wear.watchface.client.test.WatchFaceControlTestService"/>
         <service android:name="androidx.wear.watchface.client.test.TestNopCanvasWatchFaceService"/>
+        <service
+            android:name="androidx.wear.watchface.client.test.OutdatedWatchFaceControlTestService">
+            <meta-data android:name="androidx.wear.watchface.xml_version" android:value="99999" />
+            <meta-data
+                android:name="androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
+                android:resource="@xml/xml_watchface" />
+        </service>
     </application>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/OutdatedWatchFaceControlTestService.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/OutdatedWatchFaceControlTestService.kt
new file mode 100644
index 0000000..aa19f94
--- /dev/null
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/OutdatedWatchFaceControlTestService.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.client.test
+
+import android.app.Service
+import android.content.Intent
+import android.os.IBinder
+
+/**
+ * Test WatchFaceControlService which has obsolete XML version in manifest.
+ */
+public class OutdatedWatchFaceControlTestService : Service() {
+    override fun onBind(p0: Intent?): IBinder? {
+        // It is not assumed to be called
+        throw NotImplementedError()
+    }
+}
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt
index 94e1489..ad5fda7 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt
@@ -358,4 +358,22 @@
         Truth.assertThat(right.systemDataSourceFallbackDefaultType).isEqualTo(
             ComplicationType.SHORT_TEXT)
     }
+
+    @Test
+    public fun xmlVersionCompatibility() {
+        Truth.assertThat(
+            WatchFaceMetadataClient.isXmlVersionCompatible(context, context.resources)).isTrue()
+        Truth.assertThat(
+            WatchFaceMetadataClient.isXmlVersionCompatible(
+                context,
+                context.resources,
+                ComponentName(context, OutdatedWatchFaceControlTestService::class.java)
+            )).isFalse()
+        Truth.assertThat(
+            WatchFaceMetadataClient.isXmlVersionCompatible(
+                context,
+                context.resources,
+                ComponentName("non.existing.package", "non.existing.package.Service")
+            )).isFalse()
+    }
 }
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/Api30Helper.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/Api30Helper.kt
new file mode 100644
index 0000000..fac5538
--- /dev/null
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/Api30Helper.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.client
+
+import android.os.Build
+import android.os.RemoteException
+import androidx.annotation.RequiresApi
+
+internal class Api30Helper {
+    @RequiresApi(Build.VERSION_CODES.R)
+    internal companion object {
+        internal fun toRuntimeExpression(e: RemoteException) = e.rethrowAsRuntimeException()
+    }
+}
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
index 5b88032..f96a66a2 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
@@ -79,9 +79,13 @@
     @Throws(RemoteException::class)
     public fun getUserStyleSchemaDigestHash(): ByteArray = ByteArray(0)
 
-    /** The watch face's [UserStyleFlavors] if any. */
+    /**
+     * Returns the watch face's [UserStyleFlavors] if any.
+     *
+     * @throws [RuntimeException] if the watch face threw an exception while trying to service the
+     * request or there was a communication problem with watch face process.
+     */
     @WatchFaceFlavorsExperimental
-    @Throws(WatchFaceException::class)
     public fun getUserStyleFlavors(): UserStyleFlavors = UserStyleFlavors()
 
     /**
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceException.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceException.kt
index eb5f023..53d358a 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceException.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceException.kt
@@ -16,62 +16,15 @@
 
 package androidx.wear.watchface.client
 
-import android.os.DeadObjectException
+import android.os.Build
 import android.os.RemoteException
-import android.os.TransactionTooLargeException
-import androidx.annotation.IntDef
 
-/**
- * Why the remote watch face query failed.
- * @hide
- **/
-@Retention(AnnotationRetention.SOURCE)
-@IntDef(
-    WatchFaceException.WATCHFACE_DIED,
-    WatchFaceException.TRANSACTION_TOO_LARGE,
-    WatchFaceException.UNKNOWN
-)
-annotation class WatchFaceExceptionReason
-
-/**
- * The watch face threw an exception while trying to service the request.
- *
- * @property reason The [WatchFaceExceptionReason] for the exception.
- */
-public class WatchFaceException(
-    e: Exception,
-    @WatchFaceExceptionReason val reason: Int
-) : Exception(e) {
-
-    companion object {
-        /**
-         * The watchface process died. Connecting again might work, but this isn't guaranteed.
-         */
-        const val WATCHFACE_DIED = 1
-
-        /**
-         * The watchface tried to send us too much data. Currently the limit on binder
-         * transactions is 1mb. See [TransactionTooLargeException] for more details.
-         */
-        const val TRANSACTION_TOO_LARGE = 2
-
-        /**
-         * The watch face threw an exception, typically during initialization. Depending on the
-         * nature of the problem this might be a transient issue or it might occur every time
-         * for that particular watch face.
-         */
-        const val UNKNOWN = 3
-    }
-}
-
-@Throws(WatchFaceException::class)
 internal fun <R> callRemote(task: () -> R): R =
     try {
         task()
-    } catch (e: DeadObjectException) {
-        throw WatchFaceException(e, WatchFaceException.WATCHFACE_DIED)
-    } catch (e: TransactionTooLargeException) {
-        throw WatchFaceException(e, WatchFaceException.TRANSACTION_TOO_LARGE)
     } catch (e: RemoteException) {
-        throw WatchFaceException(e, WatchFaceException.UNKNOWN)
+        if (Build.VERSION.SDK_INT >= 30)
+            throw Api30Helper.toRuntimeExpression(e)
+        else
+            throw RuntimeException(e)
     }
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt
index c39005c..4c670a1 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt
@@ -21,10 +21,12 @@
 import android.content.Intent
 import android.content.ServiceConnection
 import android.content.pm.PackageManager
+import android.content.res.Resources
 import android.content.res.XmlResourceParser
 import android.graphics.RectF
 import android.os.Bundle
 import android.os.IBinder
+import android.util.Log
 import androidx.annotation.RestrictTo
 import androidx.wear.watchface.complications.ComplicationSlotBounds
 import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
@@ -55,6 +57,9 @@
 public interface WatchFaceMetadataClient : AutoCloseable {
 
     public companion object {
+        /** @hide */
+        private const val TAG = "WatchFaceMetadataClient"
+
         /**
          * Constructs a [WatchFaceMetadataClient] for fetching metadata for the specified watch
          * face.
@@ -90,11 +95,50 @@
         }
 
         /** @hide */
+        private const val ANDROIDX_WATCHFACE_XML_VERSION = "androidx.wear.watchface.xml_version"
+        /** @hide */
+        private const val ANDROIDX_WATCHFACE_CONTROL_SERVICE =
+            "androidx.wear.watchface.control.WatchFaceControlService"
+
+        @Suppress("DEPRECATION") // getServiceInfo
+        internal fun isXmlVersionCompatible(
+            context: Context,
+            resources: Resources,
+            controlServiceComponentName: ComponentName = ComponentName(
+                context, ANDROIDX_WATCHFACE_CONTROL_SERVICE)
+        ): Boolean {
+            val version = try {
+                context.packageManager.getServiceInfo(
+                    controlServiceComponentName,
+                    PackageManager.GET_META_DATA or PackageManager.MATCH_DISABLED_COMPONENTS
+                ).metaData.getInt(ANDROIDX_WATCHFACE_XML_VERSION, 0)
+            } catch (exception: PackageManager.NameNotFoundException) {
+                // WatchFaceControlService may be missing in case WF is built with
+                // pre-androidx watchface library.
+                return false
+            }
+
+            val ourVersion = resources.getInteger(
+                androidx.wear.watchface.R.integer.watch_face_xml_version)
+
+            if (version > ourVersion) {
+                Log.w(TAG, "WatchFaceControlService version ($version) " +
+                    "of $controlServiceComponentName is higher than $ourVersion")
+                return false
+            }
+
+            return true
+        }
+
+        /** @hide */
         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
         @Suppress("DEPRECATION")
         open class ParserProvider {
             // Open to allow testing without having to install the sample app.
             open fun getParser(context: Context, watchFaceName: ComponentName): XmlResourceParser? {
+                if (!isXmlVersionCompatible(context, context.resources))
+                    return null
+
                 return context.packageManager.getServiceInfo(
                     watchFaceName,
                     PackageManager.GET_META_DATA
@@ -165,8 +209,10 @@
 
     /**
      * Returns the watch face's [UserStyleSchema].
+     *
+     * @throws [RuntimeException] if the watch face threw an exception while trying to service the
+     * request or there was a communication problem with watch face process.
      */
-    @Throws(WatchFaceException::class)
     public fun getUserStyleSchema(): UserStyleSchema
 
     /**
@@ -179,14 +225,18 @@
     /**
      * Returns a map of [androidx.wear.watchface.ComplicationSlot] ID to [ComplicationSlotMetadata]
      * for each slot in the watch face's [androidx.wear.watchface.ComplicationSlotsManager].
+     *
+     * @throws [RuntimeException] if the watch face threw an exception while trying to service the
+     * request or there was a communication problem with watch face process.
      */
-    @Throws(WatchFaceException::class)
     public fun getComplicationSlotMetadataMap(): Map<Int, ComplicationSlotMetadata>
 
     /**
      * Returns the watch face's [UserStyleFlavors].
+     *
+     * @throws [RuntimeException] if the watch face threw an exception while trying to service the
+     * request or there was a communication problem with watch face process.
      */
-    @Throws(WatchFaceException::class)
     @WatchFaceFlavorsExperimental
     public fun getUserStyleFlavors(): UserStyleFlavors
 }
diff --git a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
index 47e166d..868b87e 100644
--- a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
+++ b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimeline.kt
@@ -137,8 +137,8 @@
                     complicationData.type == defaultComplicationData.type
                 ) {
                     "TimelineEntry's complicationData must have the same type as the " +
-                        "defaultComplicationData. Found ${complicationData.type} expected " +
-                        "${defaultComplicationData.type}."
+                        "defaultComplicationData or be NoDataComplicationData. Found " +
+                        "${complicationData.type} expected ${defaultComplicationData.type}."
                 }
 
                 require(!complicationData.hasPlaceholderFields()) {
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
index 0c31e4b..0dad6db 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
@@ -262,6 +262,8 @@
     private static final String FIELD_TIMELINE_START_TIME = "TIMELINE_START_TIME";
     private static final String FIELD_TIMELINE_END_TIME = "TIMELINE_END_TIME";
     private static final String FIELD_TIMELINE_ENTRIES = "TIMELINE";
+    private static final String FIELD_TIMELINE_ENTRY_TYPE = "TIMELINE_ENTRY_TYPE";
+    private static final String FIELD_PLACEHOLDER_FIELDS = "PLACEHOLDER_FIELDS";
     private static final String FIELD_PLACEHOLDER_TYPE = "PLACEHOLDER_TYPE";
     private static final String FIELD_DATA_SOURCE = "FIELD_DATA_SOURCE";
 
@@ -351,6 +353,7 @@
                     FIELD_LONG_TITLE,
                     FIELD_MAX_VALUE,
                     FIELD_MIN_VALUE,
+                    FIELD_PLACEHOLDER_FIELDS,
                     FIELD_PLACEHOLDER_TYPE,
                     FIELD_SHORT_TEXT,
                     FIELD_SHORT_TITLE,
@@ -389,19 +392,9 @@
     }
 
     ComplicationData(int type, Bundle fields) {
+        mType = type;
         mFields = fields;
         mFields.setClassLoader(getClass().getClassLoader());
-        // If this is a placeholder, coerce to TYPE_NO_DATA.
-        // If this is defined within a timeline, we assume the type of the outer ComplicationData
-        // applies to all elements in the timeline and we can just use the passed in type. The only
-        // exception is if we get a NO_DATA ComplicationData. In that case, we can check whether
-        // the placeholder type is included the serialization to determine if NO_DATA was passed
-        // in and coerce the type to NO_DATA.
-        if (mFields.containsKey(FIELD_PLACEHOLDER_TYPE)) {
-            mType = TYPE_NO_DATA;
-        } else {
-            mType = type;
-        }
     }
 
     private ComplicationData(@NonNull Parcel in) {
@@ -411,7 +404,7 @@
 
     @RequiresApi(api = Build.VERSION_CODES.P)
     private static class SerializedForm implements Serializable {
-        private static final int VERSION_NUMBER = 5;
+        private static final int VERSION_NUMBER = 6;
 
         @NonNull
         ComplicationData mComplicationData;
@@ -480,9 +473,6 @@
             if (isFieldValidForType(FIELD_END_TIME, type)) {
                 oos.writeLong(mComplicationData.getEndDateTimeMillis());
             }
-            if (isFieldValidForType(FIELD_PLACEHOLDER_TYPE, type)) {
-                oos.writeInt(mComplicationData.getPlaceholderType());
-            }
             if (isFieldValidForType(FIELD_DATA_SOURCE, type)) {
                 ComponentName componentName = mComplicationData.getDataSource();
                 if (componentName == null) {
@@ -500,6 +490,16 @@
             long end = mComplicationData.mFields.getLong(FIELD_TIMELINE_END_TIME, -1);
             oos.writeLong(end);
 
+            if (isFieldValidForType(FIELD_PLACEHOLDER_FIELDS, type)) {
+                ComplicationData placeholder = mComplicationData.getPlaceholder();
+                if (placeholder == null) {
+                    oos.writeBoolean(false);
+                } else {
+                    oos.writeBoolean(true);
+                    new SerializedForm(placeholder).writeObject(oos);
+                }
+            }
+
             // This has to be last, since it's recursive.
             List<ComplicationData> timeline = mComplicationData.getTimelineEntries();
             int timelineLength = (timeline != null) ? timeline.size() : 0;
@@ -572,12 +572,6 @@
             if (isFieldValidForType(FIELD_END_TIME, type)) {
                 fields.putLong(FIELD_END_TIME, ois.readLong());
             }
-            if (isFieldValidForType(FIELD_PLACEHOLDER_TYPE, type)) {
-                int placeholderType = ois.readInt();
-                if (placeholderType != 0) {
-                    fields.putInt(FIELD_PLACEHOLDER_TYPE, placeholderType);
-                }
-            }
             if (isFieldValidForType(FIELD_DATA_SOURCE, type)) {
                 String componentName = ois.readUTF();
                 if (componentName.isEmpty()) {
@@ -598,6 +592,18 @@
             if (end != -1) {
                 fields.putLong(FIELD_TIMELINE_END_TIME, end);
             }
+
+            if (isFieldValidForType(FIELD_PLACEHOLDER_FIELDS, type)) {
+                if (ois.readBoolean()) {
+                    SerializedForm serializedPlaceholder = new SerializedForm();
+                    serializedPlaceholder.readObject(ois);
+                    fields.putInt(FIELD_PLACEHOLDER_TYPE,
+                            serializedPlaceholder.mComplicationData.mType);
+                    fields.putBundle(FIELD_PLACEHOLDER_FIELDS,
+                            serializedPlaceholder.mComplicationData.mFields);
+                }
+            }
+
             int timelineLength = ois.readInt();
             if (timelineLength != 0) {
                 Parcelable[] parcels = new Parcelable[timelineLength];
@@ -731,11 +737,12 @@
         }
         ArrayList<ComplicationData> entries = new ArrayList<>();
         for (Parcelable parcelable : bundles) {
-            // Pass is the type of the outer complication data to the timeline entries by default.
-            // The array should only contain elements of the same type. The only exception is the
-            // NO_DATA type, which is allowed, but the code in the constructor is going to coerce
-            // the type to NO_DATA if necessary.
-            entries.add(new ComplicationData(mType, (Bundle) parcelable));
+            Bundle bundle = (Bundle) parcelable;
+            // Use the serialized FIELD_TIMELINE_ENTRY_TYPE or the outer type if it's not there.
+            // Usually the timeline entry type will be the same as the outer type, unless an entry
+            // contains NoDataComplicationData.
+            int type = bundle.getInt(FIELD_TIMELINE_ENTRY_TYPE, mType);
+            entries.add(new ComplicationData(type, (Bundle) parcelable));
         }
         return entries;
     }
@@ -747,7 +754,13 @@
         } else {
             mFields.putParcelableArray(
                     FIELD_TIMELINE_ENTRIES,
-                    timelineEntries.stream().map(e -> e.mFields).toArray(Parcelable[]::new));
+                    timelineEntries.stream().map(
+                            e -> {
+                                // This supports timeline entry of NoDataComplicationData.
+                                e.mFields.putInt(FIELD_TIMELINE_ENTRY_TYPE, e.mType);
+                                return e.mFields;
+                            }
+                    ).toArray(Parcelable[]::new));
         }
     }
 
@@ -1215,28 +1228,18 @@
     }
 
     /**
-     * Returns true if the ComplicationData contains a placeholder type. I.e. if
-     * {@link #getPlaceholderType} can succeed.
+     * Returns the placeholder ComplicationData if there is one or `null`.
      */
-    public boolean hasPlaceholderType() {
-        try {
-            return isFieldValidForType(FIELD_PLACEHOLDER_TYPE, mType)
-                    && mFields.containsKey(FIELD_PLACEHOLDER_TYPE);
-        } catch (BadParcelableException e) {
-            return false;
-        }
-    }
-
-    /**
-     * Returns the type this complication is a placeholder for.
-     *
-     * <p>Valid only if the type of this complication data is {@link #TYPE_NO_DATA}.
-     * Otherwise returns zero.
-     */
-    @ComplicationType
-    public int getPlaceholderType() {
+    @Nullable
+    public ComplicationData getPlaceholder() {
+        checkFieldValidForType(FIELD_PLACEHOLDER_FIELDS, mType);
         checkFieldValidForType(FIELD_PLACEHOLDER_TYPE, mType);
-        return mFields.getInt(FIELD_PLACEHOLDER_TYPE);
+        if (!mFields.containsKey(FIELD_PLACEHOLDER_FIELDS)
+                || !mFields.containsKey(FIELD_PLACEHOLDER_TYPE)) {
+            return null;
+        }
+        return new ComplicationData(mFields.getInt(FIELD_PLACEHOLDER_TYPE),
+                mFields.getBundle(FIELD_PLACEHOLDER_FIELDS));
     }
 
     /**
@@ -1699,13 +1702,21 @@
         }
 
         /**
-         * Sets the type this complication is a placeholder for.
+         * Sets the placeholder.
          *
          * <p>Returns this Builder to allow chaining.
          */
+        @SuppressLint("SyntheticAccessor")
         @NonNull
-        public Builder setPlaceholderType(@ComplicationType int placeholderType) {
-            putIntField(FIELD_PLACEHOLDER_TYPE, placeholderType);
+        public Builder setPlaceholder(@Nullable ComplicationData placeholder) {
+            if (placeholder == null) {
+                mFields.remove(FIELD_PLACEHOLDER_FIELDS);
+                mFields.remove(FIELD_PLACEHOLDER_TYPE);
+            } else {
+                ComplicationData.checkFieldValidForType(FIELD_PLACEHOLDER_FIELDS, mType);
+                mFields.putBundle(FIELD_PLACEHOLDER_FIELDS, placeholder.mFields);
+                putIntField(FIELD_PLACEHOLDER_TYPE, placeholder.mType);
+            }
             return this;
         }
 
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index 8520303..5141d8b 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -86,11 +86,6 @@
     internal open fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
     }
 
-    internal fun asPlaceholderWireComplicationData(): WireComplicationData =
-        WireComplicationDataBuilder(NoDataComplicationData.TYPE.toWireComplicationType()).apply {
-            fillWireComplicationDataBuilder(this)
-        }.build()
-
     /**
      * Returns `true` if any of the fields of this ComplicationData are placeholders. I.e. if any
      * fields are equal to: [ComplicationText.PLACEHOLDER], [SmallImage.PLACEHOLDER],
@@ -135,7 +130,7 @@
     TYPE,
     placeholder?.tapAction,
     cachedWireComplicationData,
-    dataSource = placeholder?.dataSource
+    dataSource = null
 ) {
 
     /** Constructs a NoDataComplicationData without a [placeholder]. */
@@ -170,12 +165,12 @@
         }
         return createWireComplicationDataBuilder().apply {
             if (placeholder == null) {
-                setPlaceholderType(TYPE.toWireComplicationType())
+                setPlaceholder(null)
             } else {
-                setPlaceholderType(placeholder.type.toWireComplicationType())
-                placeholder.fillWireComplicationDataBuilder(this)
+                val builder = placeholder.createWireComplicationDataBuilder()
+                placeholder.fillWireComplicationDataBuilder(builder)
+                setPlaceholder(builder.build())
             }
-            setDataSource(dataSource)
         }.build().also { cachedWireComplicationData = it }
     }
 
@@ -1611,6 +1606,84 @@
     }
 }
 
+internal fun WireComplicationData.toPlaceholderComplicationData(): ComplicationData? = when (type) {
+    NoDataComplicationData.TYPE.toWireComplicationType() -> null
+
+    ShortTextComplicationData.TYPE.toWireComplicationType() -> {
+        ShortTextComplicationData.Builder(
+            shortText!!.toApiComplicationTextPlaceholderAware(),
+            contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
+        ).apply {
+            setTapAction(tapAction)
+            setValidTimeRange(parseTimeRange())
+            setMonochromaticImage(parseIconPlaceholderAware())
+            setTitle(shortTitle?.toApiComplicationTextPlaceholderAware())
+            setDataSource(dataSource)
+        }.build()
+    }
+
+    LongTextComplicationData.TYPE.toWireComplicationType() -> {
+        LongTextComplicationData.Builder(
+            longText!!.toApiComplicationTextPlaceholderAware(),
+            contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
+        ).apply {
+            setTapAction(tapAction)
+            setValidTimeRange(parseTimeRange())
+            setMonochromaticImage(parseIconPlaceholderAware())
+            setSmallImage(parseSmallImagePlaceholderAware())
+            setTitle(longTitle?.toApiComplicationTextPlaceholderAware())
+            setDataSource(dataSource)
+        }.build()
+    }
+
+    RangedValueComplicationData.TYPE.toWireComplicationType() ->
+        RangedValueComplicationData.Builder(
+            value = rangedValue,
+            min = rangedMinValue,
+            max = rangedMaxValue,
+            contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
+        ).apply {
+            setTapAction(tapAction)
+            setValidTimeRange(parseTimeRange())
+            setMonochromaticImage(parseIconPlaceholderAware())
+            setTitle(shortTitle?.toApiComplicationTextPlaceholderAware())
+            setText(shortText?.toApiComplicationTextPlaceholderAware())
+            setDataSource(dataSource)
+        }.build()
+
+    MonochromaticImageComplicationData.TYPE.toWireComplicationType() ->
+        MonochromaticImageComplicationData(
+            parseIconPlaceholderAware()!!,
+            contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY,
+            tapAction,
+            parseTimeRange(),
+            this,
+            dataSource
+        )
+
+    SmallImageComplicationData.TYPE.toWireComplicationType() ->
+        SmallImageComplicationData(
+            parseSmallImagePlaceholderAware()!!,
+            contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY,
+            tapAction,
+            parseTimeRange(),
+            this,
+            dataSource
+        )
+
+    PhotoImageComplicationData.TYPE.toWireComplicationType() ->
+        PhotoImageComplicationData(
+            parseLargeImagePlaceholderAware()!!,
+            contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY,
+            tapAction,
+            parseTimeRange(),
+            this,
+            dataSource
+        )
+
+    else -> null
+}
+
 /**
  * @hide
  */
@@ -1619,95 +1692,9 @@
     val wireComplicationData = this
     return when (type) {
         NoDataComplicationData.TYPE.toWireComplicationType() -> {
-            if (hasPlaceholderType()) {
-                val placeholder = when (placeholderType) {
-                    NoDataComplicationData.TYPE.toWireComplicationType() -> null
-
-                    ShortTextComplicationData.TYPE.toWireComplicationType() -> {
-                        ShortTextComplicationData.Builder(
-                            shortText!!.toApiComplicationTextPlaceholderAware(),
-                            contentDescription?.toApiComplicationText()
-                                ?: ComplicationText.EMPTY
-                        ).apply {
-                            setMonochromaticImage(parseIconPlaceholderAware())
-                            setTitle(shortTitle?.toApiComplicationTextPlaceholderAware())
-                            setDataSource(dataSource)
-                        }.build()
-                    }
-
-                    LongTextComplicationData.TYPE.toWireComplicationType() -> {
-                        LongTextComplicationData.Builder(
-                            longText!!.toApiComplicationTextPlaceholderAware(),
-                            contentDescription?.toApiComplicationText()
-                                ?: ComplicationText.EMPTY
-                        ).apply {
-                            setMonochromaticImage(parseIconPlaceholderAware())
-                            setSmallImage(parseSmallImagePlaceholderAware())
-                            setTitle(longTitle?.toApiComplicationTextPlaceholderAware())
-                            setDataSource(dataSource)
-                        }.build()
-                    }
-
-                    RangedValueComplicationData.TYPE.toWireComplicationType() ->
-                        RangedValueComplicationData.Builder(
-                            value = rangedValue,
-                            min = rangedMinValue,
-                            max = rangedMaxValue,
-                            contentDescription?.toApiComplicationText()
-                                ?: ComplicationText.EMPTY
-                        ).apply {
-                            setMonochromaticImage(parseIconPlaceholderAware())
-                            setTitle(shortTitle?.toApiComplicationTextPlaceholderAware())
-                            setText(shortText?.toApiComplicationTextPlaceholderAware())
-                            setDataSource(dataSource)
-                        }.build()
-
-                    MonochromaticImageComplicationData.TYPE.toWireComplicationType() ->
-                        MonochromaticImageComplicationData(
-                            parseIconPlaceholderAware()!!,
-                            contentDescription?.toApiComplicationText()
-                                ?: ComplicationText.EMPTY,
-                            tapAction,
-                            parseTimeRange(),
-                            wireComplicationData,
-                            dataSource
-                        )
-
-                    SmallImageComplicationData.TYPE.toWireComplicationType() ->
-                        SmallImageComplicationData(
-                            parseSmallImagePlaceholderAware()!!,
-                            contentDescription?.toApiComplicationText()
-                                ?: ComplicationText.EMPTY,
-                            tapAction,
-                            parseTimeRange(),
-                            wireComplicationData,
-                            dataSource
-                        )
-
-                    PhotoImageComplicationData.TYPE.toWireComplicationType() ->
-                        PhotoImageComplicationData(
-                            parseLargeImagePlaceholderAware()!!,
-                            contentDescription?.toApiComplicationText()
-                                ?: ComplicationText.EMPTY,
-                            tapAction,
-                            parseTimeRange(),
-                            wireComplicationData,
-                            dataSource
-                        )
-
-                    else -> throw IllegalStateException(
-                        "Unrecognized placeholderType $placeholderType"
-                    )
-                }
-
-                if (placeholder != null) {
-                    NoDataComplicationData(placeholder)
-                } else {
-                    NoDataComplicationData()
-                }
-            } else {
-                NoDataComplicationData()
-            }
+            placeholder?.toPlaceholderComplicationData() ?.let {
+                NoDataComplicationData(it)
+            } ?: NoDataComplicationData()
         }
 
         EmptyComplicationData.TYPE.toWireComplicationType() -> EmptyComplicationData()
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
index e583843..2b79589 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
@@ -1095,16 +1095,19 @@
 
     @Test
     public fun timelineEntryCollectionWithPlaceholder() {
+        val placeholderString =
+            androidx.wear.watchface.complications.data.ComplicationText.PLACEHOLDER_STRING
         val data =
             ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
-                .setLongText(ComplicationText.plainText(
-                    androidx.wear.watchface.complications.data.ComplicationText.PLACEHOLDER_STRING))
+                .setLongText(ComplicationText.plainText(placeholderString))
                 .build()
         val timelineEntry =
             ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
-                .setPlaceholderType(ComplicationData.TYPE_LONG_TEXT)
-                .setLongText(ComplicationText.plainText(
-                    androidx.wear.watchface.complications.data.ComplicationText.PLACEHOLDER_STRING))
+                .setPlaceholder(
+                    ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT)
+                        .setLongText(ComplicationText.plainText(placeholderString))
+                            .build()
+                )
                 .build()
         timelineEntry.timelineStartEpochSecond = 100
         timelineEntry.timelineEndEpochSecond = 1000
@@ -1112,7 +1115,7 @@
 
         val entry = data.timelineEntries!!.first()
         Truth.assertThat(entry.type).isEqualTo(ComplicationData.TYPE_NO_DATA)
-        Truth.assertThat(entry.placeholderType).isEqualTo(ComplicationData.TYPE_LONG_TEXT)
+        Truth.assertThat(entry.placeholder!!.type).isEqualTo(ComplicationData.TYPE_LONG_TEXT)
     }
 
     private companion object {
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
index 19a5e93..f1945a4 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
@@ -45,7 +45,7 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                    .setPlaceholderType(WireComplicationData.TYPE_NO_DATA)
+                    .setPlaceholder(null)
                     .build()
             )
         testRoundTripConversions(data)
@@ -470,12 +470,17 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                    .setPlaceholderType(WireComplicationData.TYPE_SHORT_TEXT)
-                    .setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
-                    .setShortTitle(ComplicationText.PLACEHOLDER.toWireComplicationText())
-                    .setIcon(createPlaceholderIcon())
-                    .setContentDescription(WireComplicationText.plainText("content description"))
-                    .setDataSource(dataSourceA)
+                    .setPlaceholder(
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+                            .setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
+                            .setShortTitle(ComplicationText.PLACEHOLDER.toWireComplicationText())
+                            .setIcon(createPlaceholderIcon())
+                            .setContentDescription(
+                                WireComplicationText.plainText("content description")
+                            )
+                            .setDataSource(dataSourceA)
+                            .build()
+                    )
                     .build()
             )
         testRoundTripConversions(data)
@@ -533,10 +538,15 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                    .setPlaceholderType(WireComplicationData.TYPE_LONG_TEXT)
-                    .setLongText(WireComplicationText.plainText("text"))
-                    .setContentDescription(WireComplicationText.plainText("content description"))
-                    .setDataSource(dataSourceA)
+                    .setPlaceholder(
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+                            .setLongText(WireComplicationText.plainText("text"))
+                            .setContentDescription(
+                                WireComplicationText.plainText("content description")
+                            )
+                            .setDataSource(dataSourceA)
+                            .build()
+                    )
                     .build()
             )
         testRoundTripConversions(data)
@@ -592,13 +602,18 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                    .setPlaceholderType(WireComplicationData.TYPE_RANGED_VALUE)
-                    .setRangedValue(RangedValueComplicationData.PLACEHOLDER)
-                    .setRangedMinValue(0f)
-                    .setRangedMaxValue(100f)
-                    .setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
-                    .setContentDescription(WireComplicationText.plainText("content description"))
-                    .setDataSource(dataSourceA)
+                    .setPlaceholder(
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                            .setRangedValue(RangedValueComplicationData.PLACEHOLDER)
+                            .setRangedMinValue(0f)
+                            .setRangedMaxValue(100f)
+                            .setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
+                            .setContentDescription(
+                                WireComplicationText.plainText("content description")
+                            )
+                            .setDataSource(dataSourceA)
+                            .build()
+                    )
                     .build()
             )
         testRoundTripConversions(data)
@@ -656,10 +671,15 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                    .setPlaceholderType(WireComplicationData.TYPE_ICON)
-                    .setIcon(createPlaceholderIcon())
-                    .setContentDescription(WireComplicationText.plainText("content description"))
-                    .setDataSource(dataSourceA)
+                    .setPlaceholder(
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+                            .setIcon(createPlaceholderIcon())
+                            .setContentDescription(
+                                WireComplicationText.plainText("content description")
+                            )
+                            .setDataSource(dataSourceA)
+                            .build()
+                    )
                     .build()
             )
         testRoundTripConversions(data)
@@ -713,11 +733,16 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                    .setPlaceholderType(WireComplicationData.TYPE_SMALL_IMAGE)
-                    .setSmallImage(createPlaceholderIcon())
-                    .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_ICON)
-                    .setContentDescription(WireComplicationText.plainText("content description"))
-                    .setDataSource(dataSourceA)
+                    .setPlaceholder(
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+                            .setSmallImage(createPlaceholderIcon())
+                            .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_ICON)
+                            .setContentDescription(
+                                WireComplicationText.plainText("content description")
+                            )
+                            .setDataSource(dataSourceA)
+                            .build()
+                    )
                     .build()
             )
         testRoundTripConversions(data)
@@ -771,10 +796,15 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                    .setPlaceholderType(WireComplicationData.TYPE_LARGE_IMAGE)
-                    .setLargeImage(createPlaceholderIcon())
-                    .setContentDescription(WireComplicationText.plainText("content description"))
-                    .setDataSource(dataSourceA)
+                    .setPlaceholder(
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_LARGE_IMAGE)
+                            .setLargeImage(createPlaceholderIcon())
+                            .setContentDescription(
+                                WireComplicationText.plainText("content description")
+                            )
+                            .setDataSource(dataSourceA)
+                            .build()
+                    )
                     .build()
             )
         testRoundTripConversions(data)
@@ -842,7 +872,7 @@
     public fun noDataComplicationData() {
         assertRoundtrip(
             WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                .setPlaceholderType(WireComplicationData.TYPE_NO_DATA).build(),
+                .setPlaceholder(null).build(),
             ComplicationType.NO_DATA
         )
     }
@@ -953,11 +983,16 @@
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
             WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                .setPlaceholderType(WireComplicationData.TYPE_SHORT_TEXT)
-                .setContentDescription(WireComplicationText.plainText("content description"))
-                .setShortText(WireComplicationText.plainText("text"))
-                .setShortTitle(WireComplicationText.plainText("title"))
-                .setIcon(icon)
+                .setPlaceholder(
+                    WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+                        .setContentDescription(
+                            WireComplicationText.plainText("content description")
+                        )
+                        .setShortText(WireComplicationText.plainText("text"))
+                        .setShortTitle(WireComplicationText.plainText("title"))
+                        .setIcon(icon)
+                        .build()
+                )
                 .build(),
             ComplicationType.NO_DATA
         )
@@ -968,11 +1003,16 @@
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
             WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                .setPlaceholderType(WireComplicationData.TYPE_LONG_TEXT)
-                .setContentDescription(WireComplicationText.plainText("content description"))
-                .setLongText(WireComplicationText.plainText("text"))
-                .setLongTitle(WireComplicationText.plainText("title"))
-                .setIcon(icon)
+                .setPlaceholder(
+                    WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+                        .setContentDescription(
+                            WireComplicationText.plainText("content description")
+                        )
+                        .setLongText(WireComplicationText.plainText("text"))
+                        .setLongTitle(WireComplicationText.plainText("title"))
+                        .setIcon(icon)
+                        .build()
+                )
                 .build(),
             ComplicationType.NO_DATA
         )
@@ -983,13 +1023,18 @@
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
             WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                .setPlaceholderType(WireComplicationData.TYPE_RANGED_VALUE)
-                .setContentDescription(WireComplicationText.plainText("content description"))
-                .setRangedValue(75f)
-                .setRangedMinValue(0f)
-                .setRangedMaxValue(100f)
-                .setShortTitle(WireComplicationText.plainText("battery"))
-                .setIcon(icon)
+                .setPlaceholder(
+                    WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                        .setContentDescription(
+                            WireComplicationText.plainText("content description")
+                        )
+                        .setRangedValue(75f)
+                        .setRangedMinValue(0f)
+                        .setRangedMaxValue(100f)
+                        .setShortTitle(WireComplicationText.plainText("battery"))
+                        .setIcon(icon)
+                        .build()
+                )
                 .build(),
             ComplicationType.NO_DATA
         )
@@ -1000,10 +1045,15 @@
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
             WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                .setPlaceholderType(WireComplicationData.TYPE_SMALL_IMAGE)
-                .setSmallImage(icon)
-                .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
-                .setContentDescription(WireComplicationText.plainText("content description"))
+                .setPlaceholder(
+                    WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+                        .setSmallImage(icon)
+                        .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
+                        .setContentDescription(
+                            WireComplicationText.plainText("content description")
+                        )
+                        .build()
+                )
                 .build(),
             ComplicationType.NO_DATA
         )
@@ -1014,9 +1064,14 @@
         val icon = Icon.createWithContentUri("someuri")
         assertRoundtrip(
             WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                .setPlaceholderType(WireComplicationData.TYPE_ICON)
-                .setIcon(icon)
-                .setContentDescription(WireComplicationText.plainText("content description"))
+                .setPlaceholder(
+                    WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+                        .setIcon(icon)
+                        .setContentDescription(
+                            WireComplicationText.plainText("content description")
+                        )
+                        .build()
+                )
                 .build(),
             ComplicationType.NO_DATA
         )
@@ -1105,14 +1160,14 @@
     }
 
     @Test
-    public fun NoDataComplicationData() {
+    public fun noDataComplicationData() {
         assertThat(
             NoDataComplicationData(
                 ShortTextComplicationData.Builder(
                     ComplicationText.PLACEHOLDER,
                     ComplicationText.EMPTY
                 ).setTapAction(mPendingIntent).build()
-            ).asWireComplicationData().tapAction
+            ).asWireComplicationData().placeholder?.tapAction
         ).isEqualTo(mPendingIntent)
     }
 }
@@ -1325,8 +1380,11 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                    .setPlaceholderType(WireComplicationData.TYPE_SHORT_TEXT)
-                    .setShortText(WireComplicationText.plainText("text"))
+                    .setPlaceholder(
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
+                            .setShortText(WireComplicationText.plainText("text"))
+                            .build()
+                    )
                     .build()
             )
     }
@@ -1340,8 +1398,11 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                    .setPlaceholderType(WireComplicationData.TYPE_LONG_TEXT)
-                    .setLongText(WireComplicationText.plainText("text"))
+                    .setPlaceholder(
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_LONG_TEXT)
+                            .setLongText(WireComplicationText.plainText("text"))
+                            .build()
+                    )
                     .build()
             )
     }
@@ -1359,10 +1420,13 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                    .setPlaceholderType(WireComplicationData.TYPE_RANGED_VALUE)
-                    .setRangedValue(95f)
-                    .setRangedMinValue(0f)
-                    .setRangedMaxValue(100f)
+                    .setPlaceholder(
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                            .setRangedValue(95f)
+                            .setRangedMinValue(0f)
+                            .setRangedMaxValue(100f)
+                            .build()
+                    )
                     .build()
             )
     }
@@ -1377,8 +1441,11 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                    .setPlaceholderType(WireComplicationData.TYPE_ICON)
-                    .setIcon(icon)
+                    .setPlaceholder(
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_ICON)
+                            .setIcon(icon)
+                            .build()
+                    )
                     .build()
             )
     }
@@ -1393,9 +1460,12 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                    .setPlaceholderType(WireComplicationData.TYPE_SMALL_IMAGE)
-                    .setSmallImage(icon)
-                    .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
+                    .setPlaceholder(
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+                            .setSmallImage(icon)
+                            .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
+                            .build()
+                    )
                     .build()
             )
     }
@@ -1409,8 +1479,11 @@
         ParcelableSubject.assertThat(data.asWireComplicationData())
             .hasSameSerializationAs(
                 WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
-                    .setPlaceholderType(WireComplicationData.TYPE_LARGE_IMAGE)
-                    .setLargeImage(icon)
+                    .setPlaceholder(
+                        WireComplicationDataBuilder(WireComplicationData.TYPE_LARGE_IMAGE)
+                            .setLargeImage(icon)
+                            .build()
+                    )
                     .build()
             )
     }
diff --git a/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java b/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java
index 8ede3a8..2f790f8 100644
--- a/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java
+++ b/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java
@@ -170,6 +170,7 @@
     boolean mIsPlaceholderTitle;
     @VisibleForTesting
     boolean mIsPlaceholderText;
+    boolean mIsPlaceholder;
 
     // Drawables for rendering rounded images
     private RoundedDrawable mRoundedBackgroundDrawable = null;
@@ -284,9 +285,12 @@
         mIsPlaceholderRangedValue = false;
         mIsPlaceholderTitle = false;
         mIsPlaceholderText = false;
+        mIsPlaceholder = false;
 
         if (data.getType() == ComplicationData.TYPE_NO_DATA) {
-            if (data.hasPlaceholderType()) {
+            ComplicationData placeholder = data.getPlaceholder();
+            if (placeholder != null) {
+                data = placeholder;
                 mIsPlaceholderIcon = data.hasIcon() && ImageKt.isPlaceholder(data.getIcon());
                 mIsPlaceholderSmallImage =
                         data.hasSmallImage() && ImageKt.isPlaceholder(data.getSmallImage());
@@ -295,7 +299,7 @@
                 mIsPlaceholderRangedValue = data.hasRangedValue()
                         && data.getRangedValue()
                         == RangedValueComplicationData.PLACEHOLDER;
-                if (data.getPlaceholderType() == ComplicationData.TYPE_LONG_TEXT) {
+                if (data.getType() == ComplicationData.TYPE_LONG_TEXT) {
                     mIsPlaceholderTitle =
                             data.hasLongTitle() && data.getLongTitle().isPlaceholder();
                     mIsPlaceholderText =
@@ -308,6 +312,7 @@
                 }
                 mComplicationData = data;
                 mHasNoData = false;
+                mIsPlaceholder = true;
             } else {
                 if (!mHasNoData) {
                     // Render TYPE_NO_DATA as a short text complication with a predefined string
@@ -547,8 +552,8 @@
             float height;
             // Avoid drawing two placeholder text fields of the same length.
             if (!mSubTextBounds.isEmpty()
-                    && (mComplicationData.getPlaceholderType() == ComplicationData.TYPE_SHORT_TEXT
-                    || mComplicationData.getPlaceholderType() == ComplicationData.TYPE_LONG_TEXT)) {
+                    && (mComplicationData.getType() == ComplicationData.TYPE_SHORT_TEXT
+                    || mComplicationData.getType() == ComplicationData.TYPE_LONG_TEXT)) {
                 width = mMainTextBounds.width() * 0.4f;
                 height = mMainTextBounds.height() * 0.9f;
             } else {
@@ -659,7 +664,7 @@
             if (paintSet.isInBurnInProtectionMode() && mBurnInProtectionIcon != null) {
                 icon = mBurnInProtectionIcon;
             }
-            icon.setColorFilter(mComplicationData.hasPlaceholderType() ? PLACEHOLDER_COLOR_FILTER :
+            icon.setColorFilter(mIsPlaceholder ? PLACEHOLDER_COLOR_FILTER :
                     paintSet.mIconColorFilter);
             drawIconOnCanvas(canvas, mIconBounds, icon);
         } else if (isPlaceholder) {
@@ -768,9 +773,6 @@
         mBackgroundBoundsF.set(0, 0, mBounds.width(), mBounds.height());
         LayoutHelper currentLayoutHelper;
         int type = mComplicationData.getType();
-        if (type == ComplicationData.TYPE_NO_DATA && mComplicationData.hasPlaceholderType()) {
-            type = mComplicationData.getPlaceholderType();
-        }
         switch (type) {
             case ComplicationData.TYPE_ICON:
                 currentLayoutHelper = new IconLayoutHelper();
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 d3dd469..f3b4ae2 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
@@ -608,8 +608,10 @@
     public void placeholderLongTextIsDrawnAsPlaceholder() {
         mComplicationRenderer.setComplicationData(
                 new ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
-                        .setPlaceholderType(ComplicationData.TYPE_LONG_TEXT)
-                        .setLongText(PLACEHOLDER_COMPLICATION_TEXT)
+                        .setPlaceholder(new ComplicationData.Builder(
+                                ComplicationData.TYPE_LONG_TEXT)
+                                .setLongText(PLACEHOLDER_COMPLICATION_TEXT)
+                                .build())
                         .build(),
                 true);
 
@@ -623,8 +625,10 @@
     public void placeholderShortTextIsDrawnAsPlaceholder() {
         mComplicationRenderer.setComplicationData(
                 new ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
-                        .setPlaceholderType(ComplicationData.TYPE_SHORT_TEXT)
-                        .setShortText(PLACEHOLDER_COMPLICATION_TEXT)
+                        .setPlaceholder(new ComplicationData.Builder(
+                                ComplicationData.TYPE_SHORT_TEXT)
+                                .setShortText(PLACEHOLDER_COMPLICATION_TEXT)
+                                .build())
                         .build(),
                 true);
 
@@ -638,9 +642,11 @@
     public void placeholderLongTitleIsDrawnAsPlaceholder() {
         mComplicationRenderer.setComplicationData(
                 new ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
-                        .setPlaceholderType(ComplicationData.TYPE_LONG_TEXT)
-                        .setLongText(ComplicationText.plainText("Hi"))
-                        .setLongTitle(PLACEHOLDER_COMPLICATION_TEXT)
+                        .setPlaceholder(new ComplicationData.Builder(
+                                ComplicationData.TYPE_LONG_TEXT)
+                                .setLongText(ComplicationText.plainText("Hi"))
+                                .setLongTitle(PLACEHOLDER_COMPLICATION_TEXT)
+                                .build())
                         .build(),
                 true);
 
@@ -654,9 +660,11 @@
     public void placeholderShortTitleIsDrawnAsPlaceholder() {
         mComplicationRenderer.setComplicationData(
                 new ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
-                        .setPlaceholderType(ComplicationData.TYPE_SHORT_TEXT)
-                        .setShortText(ComplicationText.plainText("Hi"))
-                        .setShortTitle(PLACEHOLDER_COMPLICATION_TEXT)
+                        .setPlaceholder(new ComplicationData.Builder(
+                                ComplicationData.TYPE_SHORT_TEXT)
+                                .setShortText(ComplicationText.plainText("Hi"))
+                                .setShortTitle(PLACEHOLDER_COMPLICATION_TEXT)
+                                .build())
                         .build(),
                 true);
 
@@ -670,11 +678,13 @@
     public void iconIsTintedWithPlaceholderTintForPlaceholderComplication() {
         mComplicationRenderer.setComplicationData(
                 new ComplicationData.Builder(ComplicationData.TYPE_NO_DATA)
-                        .setPlaceholderType(ComplicationData.TYPE_SHORT_TEXT)
-                        .setShortText(ComplicationText.plainText("Hi"))
-                        .setIcon(Icon.createWithBitmap(
-                                Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565)))
-                        .setShortTitle(PLACEHOLDER_COMPLICATION_TEXT)
+                        .setPlaceholder(new ComplicationData.Builder(
+                                ComplicationData.TYPE_SHORT_TEXT)
+                                .setShortText(ComplicationText.plainText("Hi"))
+                                .setIcon(Icon.createWithBitmap(
+                                        Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565)))
+                                .setShortTitle(PLACEHOLDER_COMPLICATION_TEXT)
+                                .build())
                         .build(),
                 false);
 
diff --git a/wear/watchface/watchface/src/main/AndroidManifest.xml b/wear/watchface/watchface/src/main/AndroidManifest.xml
index 52231e3..546fd68 100644
--- a/wear/watchface/watchface/src/main/AndroidManifest.xml
+++ b/wear/watchface/watchface/src/main/AndroidManifest.xml
@@ -35,6 +35,8 @@
         android:exported="true"
         android:permission="com.google.android.wearable.permission.BIND_WATCH_FACE_CONTROL">
       <meta-data android:name="androidx.wear.watchface.api_version" android:value="5" />
+      <meta-data android:name="androidx.wear.watchface.xml_version"
+          android:value="@integer/watch_face_xml_version" />
       <intent-filter>
         <action android:name="com.google.android.wearable.action.WATCH_FACE_CONTROL"/>
       </intent-filter>
diff --git a/wear/watchface/watchface/src/main/res/values/config.xml b/wear/watchface/watchface/src/main/res/values/config.xml
index 2325b7c2..a0e0eec 100644
--- a/wear/watchface/watchface/src/main/res/values/config.xml
+++ b/wear/watchface/watchface/src/main/res/values/config.xml
@@ -17,4 +17,5 @@
 
 <resources>
     <bool name="watch_face_instance_service_enabled">false</bool>
+    <integer name="watch_face_xml_version">0</integer>
 </resources>